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?
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:
.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.
.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.
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
}
}
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
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