1

The Problem

In order to customize the NavigationBar in my full SwiftUI app, I had to use a bridge to UIKit using the UINavigationBarAppearance() to change the background, title attributes, etc. Now, with my dark background image I want to use a custom button tint color (white) only for the NavigationBar – which is different to the apps accent color.

My problem is that setting the bars tint color with UINavigationBar.appearance().tintColor = .white isn’t changing the button tint color at all. I’ve seen plenty of tutorials that use this line of code, but I can’t get it to work.

Does anyone know if this is a bug of iOS 16 or whether my code is wrong? Or maybe there is a good workaround for this?

My Code

import SwiftUI

struct MainNavigationBar: ViewModifier {
    init() {
        let appearance = UINavigationBarAppearance()
        
        // Custom background gradient & shadow
        appearance.backgroundImage = UIImage(named: "NavigationBarBackground")
        appearance.shadowImage = UIImage(named: "NavigationBarShadow")
        
        // Custom title styling
        appearance.titleTextAttributes = [
            .foregroundColor: UIColor.white,
            .font: UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .headline).pointSize, weight: .semibold, width: .expanded)]
        appearance.largeTitleTextAttributes = [
            .foregroundColor: UIColor.white,
            .font: UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title1).pointSize, weight: .bold, width: .expanded)]

        // Custom button coloring
        // → The following line is not working for some reason!
        UINavigationBar.appearance().tintColor = .white
        
        // Apply custom styling to all bar states
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().compactAppearance = appearance
    }
    
    func body(content: Content) -> some View {
        content
    }
}


// MARK: - View Extension for Styling
extension View {
    func mainNavBarStyle() -> some View {
        self
            .modifier(MainNavigationBar())
    }
}


// MARK: - Preview
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationStack {
            // Content
        }
        .mainNavBarStyle()
    }
}

And this is what it looks like: Screenshot of actual result


What I’ve already tried

1. SwiftUI’s .toolbarBackground

The SwiftUI native .toolbarBackground doesn’t work properly with gradients, even though it should accept LinearGradient as the ShapeStyle. Apparently this is a bug in iOS 16 according to this blog. Therefore I can’t create the initial custom background for my custom NavBar.

Combining those two approaches doesn’t work either, because .toolbarBackground or .toolbarColorScheme(.dark, for: .navigationBar) to tint the bar buttons white overrides the other background and title font changes.

2. SwiftUI’s .tint()

I’ve tried setting the tint color to the view extension (or the ViewModifier’s body) like this:

extension View {
    func mainNavBarStyle() -> some View {
        self
            .tint(.white)
            .modifier(MainNavigationBar())
    }
}

The issue is that it changes the tint color of the whole app – due to the nature of the ViewModifier overriding all subviews.

That means I would have to set the tint() back to the apps accent color in every single view of the app.

3. Extension of UINavigationController

Writing an extension of the UINavigationController didn’t help either:

extension UINavigationController {
    override open func viewDidLoad() {
        super.viewDidLoad()
        
        let appearance = UINavigationBarAppearance()

        // Other custom appearance stuff
        
        UINavigationBar.appearance().tintColor = .white
        
        // Apply custom styling to all bar states
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().compactAppearance = appearance
    }
}

4. Using UIBarButtonItemAppearance()

Thanks to the hints of Jon, Arthur and @rudrank-riyam I’ve tried using UIBarButtonItemAppearance(). Placing the following code in the init() of the ViewModifier struct from above only changed the back button and not the action buttons, though.

let appearance = UINavigationBarAppearance()
        
// Custom background & title colors
appearance.backgroundColor = .systemOrange
appearance.titleTextAttributes = [.foregroundColor: UIColor.systemRed]
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.systemRed]
        
        // Button tinting
let buttonAppearance = UIBarButtonItemAppearance()
buttonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.white]
        
// Custom back button icon
let image = UIImage(systemName: "chevron.backward")!.withTintColor(.white, renderingMode: .alwaysOriginal)
appearance.setBackIndicatorImage(image, transitionMaskImage: image)
        
appearance.buttonAppearance = buttonAppearance
appearance.backButtonAppearance = buttonAppearance
appearance.doneButtonAppearance = buttonAppearance
        
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance

Results: Results of code example

alexkaessner
  • 1,966
  • 1
  • 14
  • 39

2 Answers2

2

The following snippet should achieve what you want. It will adjust the navigation bars and its associated system buttons appearance. You can then use the tint modifier on individual bar items. e.g toolbar or navigation bar items.

import SwiftUI

struct ContentView: View {
    @State private var didSetupAppearance: Bool = false
    
    init() {
        setupAppearance()
    }
    
    var body: some View {
        NavigationView {
            Color.clear
                .listStyle(.insetGrouped)
                .navigationBarTitle("Hello there", displayMode: .automatic)
                .navigationBarItems(
                    leading: makeBarButton(
                        image: Image(systemName: "paintbrush.fill")
                    ) {
                        print("Leading Tapped…")
                    },
                    trailing: makeBarButton(
                        image: Image(systemName: "bookmark.fill")
                    ) {
                        print("Trailing Tapped…")
                    }
                )
        }
    }
    
    func makeBarButton(
        image: Image,
        onTap: @escaping () -> Void
    ) -> some View {
        Button {
            onTap()
        } label: {
            image
                .font(Font.system(size: 15, weight: .bold))
                .tint(.white)
        }
    }
    
    func setupAppearance() {
        guard !didSetupAppearance else {
            return
        }
        
        let appearance = UINavigationBarAppearance()
        appearance.configureWithTransparentBackground()
                
        // Custom background gradient & shadow
        appearance.backgroundImage = Self.makeLinearGradient(
            size: .init(width: 1, height: 1),
            colors: [.red, .yellow]
        )
        appearance.shadowImage = UIImage()
        
        // tint buttons
        let buttonAppearance = UIBarButtonItemAppearance()
        buttonAppearance.normal.titleTextAttributes = [
            .foregroundColor: UIColor.white
        ]
        appearance.buttonAppearance = buttonAppearance
        appearance.backButtonAppearance = buttonAppearance
        appearance.doneButtonAppearance = buttonAppearance
        
        // Custom title styling
        appearance.titleTextAttributes = [
            .foregroundColor: UIColor.white,
            .font: UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .headline).pointSize, weight: .semibold, width: .expanded)]
        appearance.largeTitleTextAttributes = [
            .foregroundColor: UIColor.white,
            .font: UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title1).pointSize, weight: .bold, width: .expanded)]
        
        // Apply custom styling to all bar states
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().compactAppearance = appearance
        
        didSetupAppearance = true
    }
}

private extension ContentView {
    static func makeLinearGradient(size: CGSize, colors: [UIColor]) -> UIImage {
        
        let renderer = UIGraphicsImageRenderer(size: size)
        let colors: [CGColor] = colors.map({ $0.cgColor })
        let gradient = CGGradient(
            colorsSpace: CGColorSpaceCreateDeviceRGB(),
            colors: colors as CFArray,
            locations: [0, 1]
        )
        return renderer.image { context in
            if let gradient {
                context.cgContext.drawLinearGradient(
                    gradient,
                    start: CGPoint(x: 0, y: 0),
                    end: CGPoint(x: size.width, y: size.height),
                    options: .init()
                )
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Gist: https://gist.github.com/arthurschiller/93667e49c87019c5ca14a2cb8ed15468

Customized Result

arthurschiller
  • 316
  • 2
  • 9
  • Thanks again for the suggestion, that worked for me! For the sake of completeness for others: I’ve adopted this code for iOS 16 API’s with `.toolbar()` and set the `.tint()` of every `Button()` manually now. This requires quite some extra code, but seems to be the only way of doing it as `Group()` or even the `ToolbarItem()` won’t accept a tint inside the toolbar modifier. More [details here](https://twitter.com/alexkaessner/status/1633110039564099590?s=20). – alexkaessner Mar 07 '23 at 19:40
0

I tried it with iOS 16, and indeed, it doesn't seem to be possible, and the tint will add it everywhere.

As a workaround you can create your own back button, and then set the color of it:

let appearance = UINavigationBarAppearance()
let backItemAppearance = UIBarButtonItemAppearance()
backItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.white]

let image = UIImage(systemName: "chevron.backward")!.withTintColor(.white, renderingMode: .alwaysOriginal)

appearance.setBackIndicatorImage(image, transitionMaskImage: image)
appearance.backButtonAppearance = backItemAppearance
appearance.shadowColor = .traits
appearance.backgroundColor = .traits

UINavigationBar.appearance().standardAppearance = appearance

extension UIColor {
  static var traits: UIColor {
    UIColor { (traits) -> UIColor in
      return traits.userInterfaceStyle == .dark ? .black : .white
    }
  }
}
Rudrank Riyam
  • 588
  • 5
  • 13
  • `.barTintColor` sets the background of the Navigation Bar, but I want to change the tint of the buttons. Sorry if that wasn’t clear. Will update my question – alexkaessner Mar 06 '23 at 18:33
  • Oops. Updated the answer – Rudrank Riyam Mar 06 '23 at 21:34
  • 1
    Thanks @rudrank-riyam – that is a great step forward! Though, I’ve updated my question as well, with a test of this. Because, **for some reason it only changes the back button tinting and not the color of the other action buttons.** – alexkaessner Mar 07 '23 at 09:30