2

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.

screenshots of the nav bar and tab bar for 3 screens

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?

0

1 Answer 1

3

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)
        }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much! Overriding the tint to the right color (and then back for the internals) was super helpful, and the principal position toolbar item helped me easily solve that one too. Thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.