30

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.

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. Commented Sep 15, 2021 at 19:16
  • @Yrb example added. Thanks! Commented Sep 15, 2021 at 20:40
  • Why do you have all of your views in GeometryReaders? Commented Sep 15, 2021 at 21:26
  • @Yrb 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 there Commented Sep 15, 2021 at 23:01

5 Answers 5

43

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

Sign up to request clarification or add additional context in comments.

2 Comments

@flopshot this is working for me in Xcode Beta 5.
There is a bug in SwiftUI where if you specify clear as the color, it'll be drawn grey. But the great thing is that if you leave out the color and just make the toolbar background hidden, it suppresses the bug where it changes color when any list is scrolled. So thanks!
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)
    }
}

9 Comments

Thanks! Let me give it a try
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.
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.
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.
I edited the answer.
|
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

Comments

2

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))
    }
}

Comments

-1

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()
    }
  }
}

Comments

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.