Search code examples
swiftuibackgroundinlinenavigationview

SwiftUI changing navigation bar background color for inline navigationBarTitleDisplayMode


I just started coding in SwiftUI and came across a problem. I need to give different colors to the background of the navigation bar (NavigationView). The colors will change as I go from one view to the next. I need to have this working for navigationBarTitleDisplayMode being "inline".

I tried the solutions presented in: SwiftUI update navigation bar title color but none of these solutions work fully for what I need.

  1. The solution in this reply to that post works for inline: Using UIViewControllerRepresentable. Nevertheless, when we first open the view it will show the color of the previous view for one second, before changing to the new color. I would like to avoid this and have the color displayed as soon as everything appears on screen. Is there a way to do this?

  2. This other solution will not work either: Changing UINavigation's appearance in init(), because when I set the background in init(), it will change the background of all the views in the app. Again, I need the views to have different background colors.

  3. I tried something similar to this solution: Modifying Toolbar, but it does not allow me to change the color of the navigation bar.

  4. The other solution I tried was this: Creating navigationBarColor function, which is based on: NAVIGATIONVIEW DYNAMIC BACKGROUND COLOR IN SWIFTUI. This solution works for navigationBarTitleDisplayMode "large", but when setting navigationBarTitleDisplayMode to "inline", it will show the background color of the navigation bar in a different color, as if it was covered by a gray/transparent layer. For example, the color it shows in "large" mode is: Red color in large mode But instead, it shows this color: Red color in inline mode

  5. Finally, I tried this solution: Subclassing UIViewController and configuring viewDidLayoutSubviews(), but it did not work for what I want it either.

The closest solutions for what I need are 1. and 4., but they still do not work 100%.

Would anybody know how to make any of these solutions work for navigationBarTitleDisplayMode inline, being able to change the background color of the navigation bar in different layouts, and showing the new color once the view is shown (without delays)?

Thank you!

By the way, I am using XCode 12.5.


Here is the sample code that I am using, taking example 4. as a model:

FirstView.swift

import SwiftUI

struct FirstView: View {
    @State private var selection: String? = nil
    
    var body: some View {
        
        NavigationView {
            GeometryReader { metrics in
                VStack {
                    Text("This is the first view")
                    
                    NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
                        EmptyView()
                    }
                    Button(action: {
                            self.selection = "SecondView"
                        print("Go to second view")
                    }) {
                        Text("Go to second view")
                    }
                }
            }
        }.navigationViewStyle(StackNavigationViewStyle())
        
    }
}

struct FirstView_Previews: PreviewProvider {
    static var previews: some View {
        FirstView()
    }
}

SecondView.swift

On this screen, if I use

.navigationBarTitleDisplayMode(.large)

the color will be displayed properly: Navigation bar with red color But using

.navigationBarTitleDisplayMode(.inline)

there is a blur on it: Navigation bar with some sort of blur over red color

import SwiftUI

struct SecondView: View {
    @State private var selection: String? = nil
    
    var body: some View {
        GeometryReader { metrics in
            VStack {
                Text("This is the second view")
                
                NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
                    EmptyView()
                }
                Button(action: {
                        self.selection = "ThirdView"
                    print("Go to third view")
                }) {
                    Text("Go to third view")
                }
            }
        }
        .navigationBarColor(backgroundColor: Color.red, titleColor: .black)
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct SecondView_Previews: PreviewProvider {
    static var previews: some View {
        SecondView()
    }
}

ThirdView.swift

This view displays the color properly as it is using

.navigationBarTitleDisplayMode(.large)

But if changed to

.navigationBarTitleDisplayMode(.inline)

it will show the blur on top of the color as well.

import SwiftUI

struct ThirdView: View {
    var body: some View {
        GeometryReader { metrics in
            Text("This is the third view")
        }
        .navigationBarColor(backgroundColor: Color.blue, titleColor: .black)
        .navigationBarTitleDisplayMode(.large)
    }
}

struct ThirdView_Previews: PreviewProvider {
    static var previews: some View {
        ThirdView()
    }
}

NavigationBarModifierView.swift

import SwiftUI

struct NavigationBarModifier: ViewModifier {

    var backgroundColor: UIColor?
    var titleColor: UIColor?
    

    init(backgroundColor: Color, titleColor: UIColor?) {
        self.backgroundColor = UIColor(backgroundColor)
        
        let coloredAppearance = UINavigationBarAppearance()
        coloredAppearance.configureWithTransparentBackground()
        coloredAppearance.backgroundColor = UIColor(backgroundColor)
        coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
        coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
        coloredAppearance.shadowColor = .clear
        
        UINavigationBar.appearance().standardAppearance = coloredAppearance
        UINavigationBar.appearance().compactAppearance = coloredAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
        UINavigationBar.appearance().tintColor = titleColor
    }

    func body(content: Content) -> some View {
        ZStack{
            content
            VStack {
                GeometryReader { geometry in
                    Color(self.backgroundColor ?? .clear)
                        .frame(height: geometry.safeAreaInsets.top)
                        .edgesIgnoringSafeArea(.top)
                    Spacer()
                }
            }
        }
    }
}

extension View {

    func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
        self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
    }

}

NOTE TO THE MODERATORS: Please, do not delete this post. I know similar questions were asked before, but I need an answer to this in particular which was not addressed. Please read before deleting indiscriminately, I need this for work. Also, I cannot ask questions inline in each of those solutions because I do not have the minimum 50 points in stackoverflow required to write there.


Solution

  • I think I have what you want. It is VERY touchy... It is a hack, and not terribly robust, so take as is...

    I got it to work by having your modifier return a clear NavBar, and then the solution from this answer works for you. I even added a ScrollView to ThirdView() to make sure that scrolling under didn't affect in. Also note, you lose all of the other built in effects of the bar like translucency, etc.

    Edit: I went over the code. The .navigationViewStyle was in the wrong spot. It likes to be outside of the NavigaionView(), where everything else needs to be inside. Also, I removed the part of the code setting the bar color in FirstView() as it was redundant and ugly. I hadn't meant to leave that in there.

    struct NavigationBarModifier: ViewModifier {
    
        var backgroundColor: UIColor?
        var titleColor: UIColor?
        
    
        init(backgroundColor: Color, titleColor: UIColor?) {
            self.backgroundColor = UIColor(backgroundColor)
            
            let coloredAppearance = UINavigationBarAppearance()
            coloredAppearance.configureWithTransparentBackground()
            coloredAppearance.backgroundColor = .clear // The key is here. Change the actual bar to clear.
            coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
            coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
            coloredAppearance.shadowColor = .clear
            
            UINavigationBar.appearance().standardAppearance = coloredAppearance
            UINavigationBar.appearance().compactAppearance = coloredAppearance
            UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
            UINavigationBar.appearance().tintColor = titleColor
        }
    
        func body(content: Content) -> some View {
            ZStack{
                content
                VStack {
                    GeometryReader { geometry in
                        Color(self.backgroundColor ?? .clear)
                            .frame(height: geometry.safeAreaInsets.top)
                            .edgesIgnoringSafeArea(.top)
                        Spacer()
                    }
                }
            }
        }
    }
    
    extension View {
        func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
            self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
        }
    }
    
    struct FirstView: View {
        @State private var selection: String? = nil
        
        var body: some View {
             NavigationView {
                GeometryReader { _ in
                    VStack {
                        Text("This is the first view")
                        
                        NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
                            EmptyView()
                        }
                        Button(action: {
                            self.selection = "SecondView"
                            print("Go to second view")
                        }) {
                            Text("Go to second view")
                        }
                    }
                    .navigationTitle("First")
                    .navigationBarTitleDisplayMode(.inline)
                    .navigationBarColor(backgroundColor: .red, titleColor: .black)
                }
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
    }
        
    
    struct SecondView: View {
        @State private var selection: String? = nil
        
        var body: some View {
            VStack {
                Text("This is the second view")
                
                NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
                    EmptyView()
                }
                Button(action: {
                    self.selection = "ThirdView"
                    print("Go to third view")
                }) {
                    Text("Go to third view")
                }
            }
            .navigationTitle("Second")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarColor(backgroundColor: .blue, titleColor: .black)
        }
    }
    
    struct ThirdView: View {
        var body: some View {
            ScrollView {
                ForEach(0..<50) { _ in
                    Text("This is the third view")
                }
            }
            .navigationTitle("Third")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarColor(backgroundColor: .green, titleColor: .black)
        }
    }