skip to Main Content

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

2

Answers


  1. 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
        }
      }
    }
    
    Login or Signup to reply.
  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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search