I've got a TabView
which hosts 3 tabs (Liabilities, In/Out, and Assets). Each tab has a NavigationView
in it. I want to have a different Nav Bar Appearance for each (red themed for Liabilities, white for In/Out, and green-themed for Assets).
I am able to set the background colors of the nav bars without any difficulty, using something like this:
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(Color.liabilitiesnav, for: .navigationBar)
This only lets me set the color of the background though, but I need to be able to change the colors of the other elements in the nav bar. The buttons that I add to the toolbar I can control by explicitly setting their colors, so that's no problem. But the nav title and the back button text and icon are only controllable using the global UINavigationBar.appearance()
functionality. But I don't want a global appearance, I want to configure the different tabs with different appearances. This is really important because my AccentColor
is a dark green and while that green looks nice on the back button and toolbar items on the Assets tab... it is a horrible green on red on the Liabilities tab. That's why I need to be able to control them separately.
I've tried using an .onAppear { }
mechanic to try to change the global appearance to match the current tab whenever that tab appears. For example, on the Liabilities tab, I have:
NavigationView {
List {
// stuff
}
.onAppear {
// tried it here...
NavHelper.useRedAppearance()
}
}
.onAppear {
// and tried it here as well
NavHelper.useRedAppearance()
}
However, it seems to get out of sync. It will start off correctly (Liabilities = red and Assets = green) but when I click back and forth between the Liabilities and Assets tabs, the updates seem to get out of sync and sometimes the Liabilities shows up green and sometimes the Assets shows up red. I added some print statements to the onAppear
code and I could see that the useRedAppearance()
was getting called when I clicked on the Liabilities tab and the useGreenAppearance()
was getting called when I clicked on the Assets tab... but the colors wouldn't necessarily update every time... and thus, got out of sync.
Here is a partial paste of NavHelper
just in case I'm doing something wrong in there:
class NavHelper {
static func useRedAppearance() {
let textcolor = UIColor.moneyred
let backgroundcolor = UIColor.liabilitiesnav
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = backgroundcolor
appearance.titleTextAttributes = [.foregroundColor: textcolor]
appearance.largeTitleTextAttributes = [.foregroundColor: textcolor]
let buttonAppearance = UIBarButtonItemAppearance()
buttonAppearance.normal.titleTextAttributes = [.foregroundColor: textcolor]
let image = UIImage(systemName: "chevron.backward")!.withTintColor(textcolor, renderingMode: .alwaysOriginal)
appearance.setBackIndicatorImage(image, transitionMaskImage: image)
appearance.buttonAppearance = buttonAppearance
appearance.backButtonAppearance = buttonAppearance
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance
UINavigationBar.appearance().compactScrollEdgeAppearance = appearance
}
}
How can I either (a) reliably switch the global appearance back and forth without getting out of sync, or (b) individually configure the views in the different tabs so they just have a fixed color theme?
The colors get out of sync because the UIAppearance
API cannot change the appearance of existing views. Sometimes the navigation bar is moved to the window before useRedAppearance
is called, causing it to not be affected.
If you just want to control the back button color, you can use tint
:
NavigationStack {
SomeView()
// 'tint' on the NavigationStack also affects tints of other views in the navigation stack
// so we apply another 'tint' here, to change the tint back
.tint(Color.accent)
}
// this sets the back button color to red
.tint(.red)
If you also want to change the color of the title, place a coloured Text
as the .principal
tool bar item.
.toolbar {
ToolbarItem(placement: .principal) {
Text("Liabilities")
.foregroundStyle(.yellow)
}
}
Alternatively, you can use a UIViewControllerRepresentable
to get a hold of the UINavigationController
and set the colors of that.
struct NavigationAppearance: UIViewControllerRepresentable {
var textColor: UIColor
var backgroundColor: UIColor
class VC: UIViewController {
var textColor: UIColor = .clear
var backgroundColor: UIColor = .clear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateBarAppearance()
}
func updateBarAppearance() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = backgroundColor
appearance.titleTextAttributes = [.foregroundColor: textColor]
appearance.largeTitleTextAttributes = [.foregroundColor: textColor]
let buttonAppearance = UIBarButtonItemAppearance()
buttonAppearance.normal.titleTextAttributes = [.foregroundColor: textColor]
let image = UIImage(systemName: "chevron.backward")!.withTintColor(textColor, renderingMode: .alwaysOriginal)
appearance.setBackIndicatorImage(image, transitionMaskImage: image)
appearance.buttonAppearance = buttonAppearance
appearance.backButtonAppearance = buttonAppearance
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
navigationController?.navigationBar.compactAppearance = appearance
navigationController?.navigationBar.compactScrollEdgeAppearance = appearance
}
}
func makeUIViewController(context: Context) -> VC {
VC()
}
func updateUIViewController(_ uiViewController: VC, context: Context) {
uiViewController.textColor = textColor
uiViewController.backgroundColor = backgroundColor
}
}
All you need to do then is to put this in the view hierarchy somewhere, e.g. background
NavigationStack {
SomeView()
.navigationTitle("Assets")
.navigationBarTitleDisplayMode(.inline)
.background {
NavigationAppearance(textColor: .systemRed, backgroundColor: .systemGreen)
}
}