SwiftUI Map breaks UINavigationBar Appearance
In Running Track, I have a screen that shows details about a run, including a map of the route. I’ve been rewriting parts of the app in SwiftUI recently, and that screen has given me a headache!
Styling Globally With UINavigationBarAppearance
To have a consistent look across the app, I set UINavigationBarAppearance
on launch. This gives me a standard background colour, tint colour and title font.
static func setNavigationBarAppearance() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(.primaryColor)
appearance.tintColor = UIColor(.darkBackgroundText)
appearance.titleTextAttributes = [
.foregroundColor: UIColor(.darkBackgroundText),
.font: RTTextStyle.display4.font
]
appearance.largeTitleTextAttributes = [
.foregroundColor: UIColor(.darkBackgroundText),
.font: RTTextStyle.display2.font
]
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
There’s no equivalent of this in SwiftUI. No way to set navigation bar style globally. (I don’t know why not.)
The Problem in SwiftUI
For some reason, if you have a SwiftUI Map
in your UI, it will override any global UINavigationBarAppearance
you set. When the Map
is in my View
, it sets the navigation bar to the default style, a white background in light mode and a dark one in dark mode. (This applies whether you use NavigationStack
or NavigationView
.)
That means you can’t use UINavigationBarAppearance
to style the navigation bar on that screen.
Solutions
There are two fixes for this, and both involve giving up on UINavigationBarAppearance
and moving to the SwiftUI method of styling individual views explicitly: 1. Style this navigation bar explicitly, and leave the rest to UINavigationBarAppearance
. 2. Get rid of UINavigationBarAppearance
and explicitly style every view that lives in a NavigationStack
.
You can style a navigation bar in SwiftUI with modifiers, but you have to apply them to each view individually:
someView
.toolbarBackground(Color(.primary), for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar) // otherwise it's hidden until you scroll
.toolbarColorScheme(.dark, for: .navigationBar) // a hack to get white text and button tints
My Solution
I’m not able to go with the first solution, modifying only the View
containing the map, because SwiftUI doesn’t give the same control over navigation bar styling that UIKit does.
For example, I change the text colour and font in my UINavigationBarAppearance
setup. But can’t change these in SwiftUI’s navigation bar. So I can’t have an exception just for the screen containing the map, without it looking different to the rest of the app. I need to completely change my navigation bar style.
So I’ve had to go nuclear and get rid of UINavigationBarAppearance
.
I created a modifier to reduce some of the repetition, but I have to set it on every View
that's presented in a NavigationStack
:
struct NavigationBarStyle: ViewModifier {
func body(content: Content) -> some View {
content
.toolbarBackground(Color(.primary), for: .navigationBar)
// otherwise it's hidden until you scroll
.toolbarBackground(.visible, for: .navigationBar)
// a hack to get white text and button tints
.toolbarColorScheme(.dark, for: .navigationBar)
}
}
extension View {
func navigationBarStyle() -> some View {
self.modifier(NavigationBarStyle())
}
}
If anyone has a better solution to this, let me know!
(Note: This issue is specific to rendering a Map
, so if you're not using a Map
in your UI you may not need to worry about this.)
If you liked this article, please consider buying me a coffee to support my writing.