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
barTintColorassignment running after your appearance code. Remove it. - Check that you set
scrollEdgeAppearanceas well asstandardAppearance. - Check whether the view controller’s
navigationItemappearance is overriding the bar. - Check that you are styling the right navigation controller. In a tab bar app,
navigationControlleron 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
barTintColorwithUINavigationBarAppearance; pick one and delete the other. - Always set all four appearance slots, including
compactScrollEdgeAppearancebehind an availability check. - Configure app-wide appearance before the first view controller loads, not in
viewWillAppear. - Use semantic colors such as
.systemIndigoso 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.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
