Fix: UINavigationBar Background Color Not Changing in Swift

Fix: UINavigationBar Background Color Not Changing in Swift

Written by

in

Quick answer: Legacy properties like barTintColor and backgroundColor are overridden by UIKit’s appearance system. To set a persistent background color, instantiate a UINavigationBarAppearance, call configureWithOpaqueBackground(), set its backgroundColor, and assign it to both navigationBar.standardAppearance and navigationBar.scrollEdgeAppearance.

Why this happens

Setting navigationBar.barTintColor or navigationBar.backgroundColor directly on a UINavigationController often produces no visible change, causes the bar to render with an unexpected translucent tint, or causes the color to snap back to default as soon as a UIScrollView or UITableView scrolls beneath it.

UIKit uses the UINavigationBarAppearance API to manage bar styling across different presentation states. A navigation bar evaluates distinct appearance configurations depending on whether content is scrolling under it (scrollEdgeAppearance), sitting at a resting position (standardAppearance), or rendered in a compact horizontal size class (compactAppearance).

Starting in iOS 13 and expanded in iOS 15, UIKit defaults scrollEdgeAppearance to a transparent background style when content sits at the top edge of a scrollable view. When you assign colors using legacy properties such as barTintColor, UIKit either ignores the assignment or fails to propagate it across all active scroll states. Resolving the issue requires defining an explicit appearance object and assigning it to all relevant appearance slots.

How to fix it

1. Configure standardAppearance and scrollEdgeAppearance with UINavigationBarAppearance

The standard way to apply a background color to a UINavigationBar in modern UIKit is through UINavigationBarAppearance. You must assign your configuration to both standardAppearance and scrollEdgeAppearance to ensure consistency before and after scrolling.

Place this configuration in your custom UINavigationController subclass, or inside viewDidLoad of the root view controller managing the navigation stack:

import UIKit

class CustomNavigationController: UINavigationController {
    override func viewDidLoad() {
        super.viewDidLoad()
        configureNavigationBarAppearance()
    }

    private func configureNavigationBarAppearance() {
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = UIColor.systemTeal

        // Configure title text attributes
        appearance.titleTextAttributes = [
            .foregroundColor: UIColor.white,
            .font: UIFont.boldSystemFont(ofSize: 18)
        ]
        appearance.largeTitleTextAttributes = [
            .foregroundColor: UIColor.white
        ]

        // Apply to standard, compact, and scroll edge states
        navigationBar.standardAppearance = appearance
        navigationBar.scrollEdgeAppearance = appearance
        navigationBar.compactAppearance = appearance

        // Tint color controls interactive items like back buttons
        navigationBar.tintColor = .white
    }
}

Calling configureWithOpaqueBackground() strips the default dynamic blur effect and ensures your chosen backgroundColor renders as an opaque surface.

2. Set the global appearance proxy in the AppDelegate or SceneDelegate

If your application uses the same navigation bar style across all screens, configure the global proxy via UINavigationBar.appearance(). This centralizes navigation styling and applies the color to every UINavigationBar created by your app.

Add this setup to application(_:didFinishLaunchingWithOptions:) in your AppDelegate or inside scene(_:willConnectTo:options:) in your SceneDelegate:

import UIKit

func setupGlobalNavigationBarTheme() {
    let appearance = UINavigationBarAppearance()
    appearance.configureWithOpaqueBackground()
    appearance.backgroundColor = UIColor(named: "BrandPrimary") ?? .systemBlue

    appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
    appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]

    let navBarProxy = UINavigationBar.appearance()
    navBarProxy.standardAppearance = appearance
    navBarProxy.scrollEdgeAppearance = appearance
    navBarProxy.compactAppearance = appearance
    navBarProxy.tintColor = .white
}

Global proxy updates take effect on newly instantiated view hierarchies. If you invoke this method after views have loaded, trigger a layout update or apply the appearance directly to the active navigation bar instance.

3. Handle SwiftUI navigation bar background colors

If you are encountering this issue in a mixed UIKit and SwiftUI app or a pure SwiftUI view hierarchy using NavigationStack, attempting to style the navigation bar by querying UINavigationBar.appearance() can produce timing glitches. Instead, use native SwiftUI modifiers on the root view inside the stack:

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(1...20, id: \.self) { item in
                Text("Item \(item)")
            }
            .navigationTitle("Inbox")
            .toolbarBackground(Color.teal, for: .navigationBar)
            .toolbarBackground(.visible, for: .navigationBar)
            .toolbarColorScheme(.dark, for: .navigationBar)
        }
    }
}

Using .toolbarBackground(.visible, for: .navigationBar) prevents SwiftUI from collapsing the bar background into transparent material when content rests at the top edge.

4. Configure the appearance object in Xcode Storyboards

If your interface relies on Storyboards or XIB files, you can configure these appearance layers directly in Interface Builder without writing code:

  1. Open your Storyboard and select the Navigation Bar inside your Navigation Controller.
  2. In the Attributes Inspector, locate the Scroll Edge Appearance checkbox and ensure it is checked.
  3. Expand the Standard Appearance section and set Background to your desired color.
  4. Expand the Scroll Edge Appearance section and set its Background color to match the Standard Appearance background.
  5. Change the Tint property in the Navigation Bar attributes to style bar button items and back buttons.

How to prevent it

  • Avoid modifying navigationBar.barTintColor or navigationBar.backgroundColor directly in code targeting modern iOS versions.
  • Always assign customized UINavigationBarAppearance instances to both standardAppearance and scrollEdgeAppearance simultaneously.
  • Set navigationBar.isTranslucent = false only if you specifically want to disable translucent content underlapping without using UINavigationBarAppearance.
  • Test views containing UITableView and UICollectionView by scrolling up and down in the Simulator to confirm the bar color does not flicker or reset when crossing scroll boundaries.

FAQ

Why does my bar background turn white or transparent when scrolling begins?

This happens when you assign your custom configuration to scrollEdgeAppearance but leave standardAppearance set to the system default, or vice versa. Assign the appearance instance to both properties to maintain uniform color throughout the scroll lifecycle.

What is the difference between tintColor and backgroundColor on UINavigationBar?

The backgroundColor (or appearance backgroundColor) controls the surface color of the bar itself. The tintColor controls the interactive elements resting inside the bar, such as bar button items, back button arrows, and action icons.

How do I make the status bar text white when using a dark navigation bar color?

When using UINavigationBarAppearance, set navigationController.navigationBar.barStyle = .black, or override preferredStatusBarStyle in your view controller to return .lightContent. Ensure UIViewControllerBasedStatusBarAppearance is set to YES in your project’s Info.plist.

Sources & credits

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