How to Change the Navigation Bar Color in Swift (UIKit and SwiftUI)

How to Change the Navigation Bar Color in Swift (UIKit and SwiftUI)

Written by

in

Quick answer: In UIKit, build a UINavigationBarAppearance, set its backgroundColor and titleTextAttributes, then assign it to the bar’s standardAppearance, compactAppearance and scrollEdgeAppearance. In SwiftUI on iOS 16 or later, use .toolbarBackground(.indigo, for: .navigationBar) with .toolbarBackground(.visible, for: .navigationBar). Setting barTintColor alone no longer works because the appearance objects take priority.

Why this happens

Two API changes broke most of the old snippets. UINavigationBar.barTintColor was deprecated in iOS 13 in favour of UINavigationBarAppearance, and since iOS 15 the bar also reads a scroll edge appearance whenever content is scrolled to the top. If the bar already has appearance objects configured — which is the default with a modern SDK — writing to barTintColor changes nothing you can see.

The older examples also mix Swift eras. UIColor.greenColor() is Swift 2 syntax, and UITextAttributeTextColor was replaced by NSAttributedString.Key.foregroundColor long ago. UINavigationBar.appearance().backgroundColor is a plain UIView property, and the bar paints its own background over it, so it is unreliable at best.

SwiftUI has a separate story. Before iOS 16 there was no public modifier for the bar background, so the usual workaround was to configure the UIKit appearance proxy from init() or onAppear. On iOS 16 and later you should use .toolbarBackground instead.

How to fix it

1. UIKit: use UINavigationBarAppearance (iOS 13+)

Create one appearance object, style it, then hand it to every slot the bar can use. Missing a slot is the most common reason a color only shows up sometimes.

import UIKit

func applyNavigationBarStyle(to navigationController: UINavigationController) {
    let appearance = UINavigationBarAppearance()
    appearance.configureWithOpaqueBackground()
    appearance.backgroundColor = .systemIndigo
    appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
    appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]

    let bar = navigationController.navigationBar
    bar.standardAppearance = appearance
    bar.compactAppearance = appearance
    bar.scrollEdgeAppearance = appearance
    if #available(iOS 15.0, *) {
        bar.compactScrollEdgeAppearance = appearance
    }

    // Back button, Done button, and other bar items
    bar.tintColor = .white
}

What each slot covers:

  • standardAppearance — the normal, full-height bar.
  • compactAppearance — the shorter bar used in landscape on iPhone.
  • scrollEdgeAppearance — the bar when its scroll view is at the top edge. This is the one that silently overrides your color on iOS 15+.
  • compactScrollEdgeAppearance — the compact version of the same thing, iOS 15+.

configureWithOpaqueBackground() removes translucency and gives you a solid fill. If you want the blurred, translucent look instead, call configureWithDefaultBackground() and set backgroundColor to a color with alpha.

2. Apply the style app-wide with the appearance proxy

If every screen should share the same bar, configure UINavigationBar.appearance() once, before any view controller is loaded — typically in application(_:didFinishLaunchingWithOptions:) or in your SceneDelegate.

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = .systemIndigo
        appearance.titleTextAttributes = [.foregroundColor: UIColor.white]

        let proxy = UINavigationBar.appearance()
        proxy.standardAppearance = appearance
        proxy.compactAppearance = appearance
        proxy.scrollEdgeAppearance = appearance
        if #available(iOS 15.0, *) {
            proxy.compactScrollEdgeAppearance = appearance
        }
        proxy.tintColor = .white

        return true
    }
}

Appearance proxy values are read when a view enters the window, so setting them after the navigation controller has already been pushed can leave the first screen unstyled.

3. Override the color for a single screen

A view controller can carry its own appearance through navigationItem. This wins over the navigation bar’s own settings, which makes it the cleanest way to give one screen a different color.

override func viewDidLoad() {
    super.viewDidLoad()

    let appearance = UINavigationBarAppearance()
    appearance.configureWithOpaqueBackground()
    appearance.backgroundColor = .systemTeal
    appearance.titleTextAttributes = [.foregroundColor: UIColor.white]

    navigationItem.standardAppearance = appearance
    navigationItem.compactAppearance = appearance
    navigationItem.scrollEdgeAppearance = appearance
    if #available(iOS 15.0, *) {
        navigationItem.compactScrollEdgeAppearance = appearance
    }
}

4. SwiftUI on iOS 16+: .toolbarBackground

On iOS 16 and later, style the bar directly on the content inside NavigationStack:

import SwiftUI

struct SettingsView: View {
    var body: some View {
        NavigationStack {
            List {
                Text("General")
                Text("Privacy")
            }
            .navigationTitle("Settings")
            .toolbarBackground(.indigo, for: .navigationBar)
            .toolbarBackground(.visible, for: .navigationBar)
            .toolbarColorScheme(.dark, for: .navigationBar)
        }
    }
}

.toolbarBackground(.visible, for: .navigationBar) is the part people forget. Without it the bar stays transparent when the scroll view is at the top, and your color only appears after scrolling. .toolbarColorScheme(.dark, for: .navigationBar) flips the title and bar button items to light text so they stay readable on a dark fill.

5. SwiftUI on iOS 15 and earlier, or mixed UIKit

If you still support iOS 15, or your SwiftUI view is hosted inside a UINavigationController, configure the appearance proxy from the view’s init():

import SwiftUI
import UIKit

struct RootView: View {
    init() {
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = .systemIndigo
        appearance.titleTextAttributes = [.foregroundColor: UIColor.white]

        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
    }

    var body: some View {
        NavigationView {
            List { Text("Row") }
                .navigationTitle("Settings")
        }
    }
}

Keep this in one place. A SwiftUI View‘s init() can run more than once, so doing heavy work there is wasteful even though the assignment itself is cheap.

6. The tab bar uses the same pattern

UITabBar moved to UITabBarAppearance in the same release, and scrollEdgeAppearance arrived on the tab bar in iOS 15.

let tabAppearance = UITabBarAppearance()
tabAppearance.configureWithOpaqueBackground()
tabAppearance.backgroundColor = .systemBrown

tabBarController?.tabBar.standardAppearance = tabAppearance
if #available(iOS 15.0, *) {
    tabBarController?.tabBar.scrollEdgeAppearance = tabAppearance
}

tabBarController?.tabBar.tintColor = .yellow // selected item

7. If the color still does not change

  • Check for a leftover barTintColor assignment running after your appearance code. Remove it.
  • Check that you set scrollEdgeAppearance as well as standardAppearance.
  • Check whether the view controller’s navigationItem appearance is overriding the bar.
  • Check that you are styling the right navigation controller. In a tab bar app, navigationController on a child screen is not the same object as the tab’s root navigation controller.
  • Check whether the bar is hidden and you are actually looking at a large title area drawn by the scroll view.

How to prevent it

  • Never mix barTintColor with UINavigationBarAppearance; pick one and delete the other.
  • Always set all four appearance slots, including compactScrollEdgeAppearance behind an availability check.
  • Configure app-wide appearance before the first view controller loads, not in viewWillAppear.
  • Use semantic colors such as .systemIndigo so the bar still looks right in dark mode and with increased contrast.
  • Test with a large title, with content scrolled to the top, and in landscape, since those are the three states that expose a missing slot.

FAQ

Why does barTintColor no longer work?

It was deprecated in iOS 13 in favour of UINavigationBarAppearance, and since iOS 15 the bar prefers its scroll edge appearance when content is at the top. If appearance objects exist, barTintColor is ignored.

How do I change only the title color?

Set titleTextAttributes on the appearance object, using NSAttributedString.Key.foregroundColor. For large titles, set largeTitleTextAttributes too, otherwise the small and large title states will use different colors.

Can I give each screen its own navigation bar color?

Yes. Assign the appearance to navigationItem.standardAppearance, compactAppearance and scrollEdgeAppearance inside that view controller. The navigation item’s appearance takes priority over the navigation bar’s own settings.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.