12

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.

Tomas
  • 153
  • 1
  • 1
  • 12
  • We can’t really answer your question with the information given. Please see: [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask) and [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/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. – Yrb Sep 15 '21 at 19:16
  • @Yrb example added. Thanks! – Tomas Sep 15 '21 at 20:40
  • Why do you have all of your views in `GeometryReaders`? – Yrb Sep 15 '21 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 – Tomas Sep 15 '21 at 23:01

5 Answers5

12

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(.yellow, 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

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
8

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)
    }
}
Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Thanks! Let me give it a try – Tomas Sep 15 '21 at 23:01
  • 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. – Tomas Sep 15 '21 at 23:35
  • 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. – Tomas Sep 15 '21 at 23:37
  • 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. – Yrb Sep 16 '21 at 02:58
  • I edited the answer. – Yrb Sep 16 '21 at 14:46
  • Sorry, just got back. Thanks for your help! I mark your solution as accepted (very well deserved). – Tomas Sep 16 '21 at 18:16
  • and also, thanks for the edition to your solution! – Tomas Sep 16 '21 at 18:18
  • 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. – Jaidyn Belbin Oct 04 '22 at 02:02
3

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

Burcu Kutluay
  • 472
  • 6
  • 14
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))
    }
}
Paul B
  • 3,989
  • 33
  • 46
1

Just add this to the end of your NavigationStack:

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

literally just that for me, hehe.

theshoperr
  • 11
  • 2