SwiftUI changing navigation bar background color for inline navigationBarTitleDisplayMode
Asked Answered
C

6

23

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.

Centrist answered 15/9, 2021 at 16:9 Comment(4)
We can’t really answer your question with the information given. Please see: How do I ask a good question? and How to create a Minimal, Reproducible Example. My advice would be to put together an example showing your attempt and then let users work on it. Otherwise, this becomes a general discussion without any specifics and nothing gets solved.Impeccant
@Impeccant example added. Thanks!Centrist
Why do you have all of your views in GeometryReaders?Impeccant
@Impeccant the GeometryReaders were added because we need to measure the size of the screen due to the screens that the graphic designers made. We constantly need to measure proportions and so on. I tried removing GeometryReaders from all places, but the problem is still thereCentrist
I
10

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)
    }
}
Impeccant answered 15/9, 2021 at 22:11 Comment(9)
Thanks! Let me give it a tryCentrist
Awesome, I think this might actually work. I gave it a quick try and made a few small changes and it looks good. I have two more questions though. Do you know how to make it look in iPad as it does in iPhone? I used to fix that with .navigationViewStyle(StackNavigationViewStyle()) but it does not do anything here. When if first loads in iPad, it comes up with a back button that when pressed will bring up the view from the left, kind of like a sidebar.Centrist
The second question is if you get warnings about constraints when building, and if you know how to fix them (just getting picky here since it is the clean remake of a project I am working on). I am getting these warnings: [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it.Centrist
As to the iPad issue, there are answers on SO. I am away from my computer to check. It is easily resolved. As to the constraints, you can ignore that. It is under the hood stuff with SwiftUI that doesn’t affect anything.Impeccant
I edited the answer.Impeccant
Sorry, just got back. Thanks for your help! I mark your solution as accepted (very well deserved).Centrist
and also, thanks for the edition to your solution!Centrist
It's really frustrating that once you've set the title colour once, there doesn't seem to be a way to change it on child views.Spheroidicity
Unfortunately this does not seem to work correctly in iOS 17. Instead, you must attach it to the NavigationView itself. I also added an onDisappear to the NavigationBarModifier that resets the bar back to its default colors.Cottage
R
28

iOS 16

You can set any color to the background color of any toolbar background color (including the navigation bar) for the inline state with these two simple native modifiers (both needed):

Xcode 14
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(.indigo, for: .navigationBar)

Notes:

  1. The color will be set on the entire bar (up to the top edge of the screen).

  2. toolbarBackground MUST be visible to see the color

  3. both modifiers should be applied on the content, NOT the NavigationStack (or NavigationView) itself!

This works for both large and inline navigationBarTitleDisplayMode.

Demo

Ramonramona answered 20/6, 2022 at 6:42 Comment(3)
This is not working for me in Xcode 14 Beta1 or Beta 2. Is anyone else not able to make it work?Expulsion
@Expulsion this is working for me in Xcode Beta 5.Backbreaker
It works for me to have those modifiers, although it is a little bit confusing that you put .yellow as your color, and the demo you showed is purple.Nila
I
10

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)
    }
}
Impeccant answered 15/9, 2021 at 22:11 Comment(9)
Thanks! Let me give it a tryCentrist
Awesome, I think this might actually work. I gave it a quick try and made a few small changes and it looks good. I have two more questions though. Do you know how to make it look in iPad as it does in iPhone? I used to fix that with .navigationViewStyle(StackNavigationViewStyle()) but it does not do anything here. When if first loads in iPad, it comes up with a back button that when pressed will bring up the view from the left, kind of like a sidebar.Centrist
The second question is if you get warnings about constraints when building, and if you know how to fix them (just getting picky here since it is the clean remake of a project I am working on). I am getting these warnings: [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it.Centrist
As to the iPad issue, there are answers on SO. I am away from my computer to check. It is easily resolved. As to the constraints, you can ignore that. It is under the hood stuff with SwiftUI that doesn’t affect anything.Impeccant
I edited the answer.Impeccant
Sorry, just got back. Thanks for your help! I mark your solution as accepted (very well deserved).Centrist
and also, thanks for the edition to your solution!Centrist
It's really frustrating that once you've set the title colour once, there doesn't seem to be a way to change it on child views.Spheroidicity
Unfortunately this does not seem to work correctly in iOS 17. Instead, you must attach it to the NavigationView itself. I also added an onDisappear to the NavigationBarModifier that resets the bar back to its default colors.Cottage
S
4

For my custom view the following code worked well.

struct HomeView: View {

    init() {
        //Use this if NavigationBarTitle is with Large Font
        UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
        
        //Use this if NavigationBarTitle is with displayMode = .inline
        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
        UINavigationBar.appearance().backgroundColor = UIColor.clear
        UINavigationBar.appearance().barTintColor = UIColor(Color(red: 32 / 255, green: 72 / 255, blue: 63 / 255))
    }

    var body: some View {
        NavigationView {
            ZStack {
            ...
            ...
            ...
                    }
                .padding(.zero)
                .navigationTitle("Feedbacks")
                }
             }
}

and result is like that:

enter image description here

Spiky answered 12/7, 2022 at 9:19 Comment(0)
K
1

Here is a bit hacky solution, but it works for me (as of iOS 15) both for .large and .inline display modes.

import SwiftUI

enum Kind: String, CaseIterable {
    case checking
    case savings
    case investment
}

struct PaddedList: View {
    @Binding var name: String
    @Binding var kind: Kind
    
    var body: some View {
        NavigationView {
            List {
                TextField("Account name", text: $name)
                Picker("Kind", selection: $kind) {
                    ForEach(Kind.allCases, id: \.self) { kind in
                        Text(kind.rawValue).tag(kind)
                    }
                }
                .listRowSeparatorTint(.red)
                Spacer()
            }
            .padding(.top, 1) // note top 1 padding!
            .background(.green) // the color "bleeds" through
            .navigationBarTitle("Navigation Bar")
        }
    }
}

struct PaddedList_Previews: PreviewProvider {
    static var previews: some View {
        PaddedList(name: .constant(""), kind: .constant(.checking))
    }
}
Karyolysis answered 29/5, 2022 at 16:32 Comment(0)
G
1

Just add this to the end of your NavigationStack:

.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.blue, for: .navigationBar)

literally just that for me, hehe.

Guido answered 14/4, 2023 at 20:45 Comment(0)
S
0

The best approach to consistent styling in our codebase might be using the the view modifiers together with a view extension.

extension View {
  func navigationBarBackground(_ background: Color = .orange) -> some View {
    return self
      .modifier(ColoredNavigationBar(background: background))
  }
}

struct ColoredNavigationBar: ViewModifier {
  var background: Color
  
  func body(content: Content) -> some View {
    content
      .toolbarBackground(
        background,
        for: .navigationBar
      )
      .toolbarBackground(.visible, for: .navigationBar)
  }
}

Now we can use navigationBarBackground() function to style the navigation bar.

struct ColoredNavigationBarDemo: View {

  var body: some View {
    NavigationStack {
      ScrollView {
        Text("Hi there!")
          .padding()
      }
      .navigationBarTitle("Demo")
      
      .navigationBarBackground()
    }
  }
}
Scandinavia answered 30/1 at 14:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.