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:
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
2
Answers
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:
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.
Gist: https://gist.github.com/arthurschiller/93667e49c87019c5ca14a2cb8ed15468