In my app, users can select from a range of UI color-palettes to customize the look of the app to suite their preference.
The app's root viewcontroller is a UINavigationController
(this app is not using SwiftUI,
nor has it adopted UIScene
yet).
However, I noticed only the first time (to be specific: this is in the appDelegate's application(:didFinishLaunchingWithOptions:)
, so before anything is actually rendered to the screen) the appearance settings are applied, but subsequent changes do not take effect.
However, the following code seems to 'fix' the issue:
self.window?.rootViewController = nil
self.updateAppearance()
self.window?.rootViewController = navigationController
This to me seems hacky/kludgy and unlikely to be a sustainable 'fix', and I think it indicates that the appearance information is only taken into account when a view(controller) gets inserted into the view(controller) hierarchy, in other words: when it gets drawn. So then the question would be: how to tell a UINavigationController / Bar to redraw using the updated appearance settings?
I'm sure I'm overlooking something relatively simple (as I imagine this kind of thing is something many apps do to support darkmode), but alas, I can't seem to find it.
I setup a small test-project to isolate this issue. It has only one custom viewcontroller (ContentViewController
) which only sets the backgroundColor in viewDidLoad()
, and an AppDelegate
(code below). No storyboards, no UIScene
related things.
So, here is the minimal code that reproduces my issue:
AppDelegate.swift
:
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
var rootNavigationController: UINavigationController?
let contentViewController = ViewController()
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
window = UIWindow()
let navigationController = UINavigationController(rootViewController: contentViewController)
rootNavigationController = navigationController
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
// initial setup of appearance, this first call will take effect as expected
updateAppearance()
// setup a timer to repeatedly change the appearance of the navigationbar
timer.setEventHandler
{
/// ! uncommenting this line and the one after `self.updateAppearance()`
/// will cause `self.updateAppearance()` to take effect as expected
// self.window?.rootViewController = nil
self.updateAppearance()
//self.window?.rootViewController = navigationController
}
timer.schedule(deadline: .now(),
repeating: .seconds(2),
leeway: .milliseconds(50))
timer.resume()
return true
}
// some vars to automatically toggle between colors / textstyles
var colorIndex = 0
let colors = [ UIColor.blue, UIColor.yellow]
let textStyles: [UIFont.TextStyle] = [.title3, .body]
func updateAppearance()
{
let color = colors[colorIndex]
let textStyle = textStyles[colorIndex]
contentViewController.title = "bgColor: \(colorIndex) \(textStyle.rawValue)"
colorIndex = (colorIndex == 0) ? 1 : 0
let textColor = colors[colorIndex]
self.rootNavigationController?.navigationBar.isTranslucent = false
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithOpaqueBackground()
navigationBarAppearance.backgroundColor = color
let textAttributes: [NSAttributedString.Key : Any] =
[.font: UIFont.preferredFont(forTextStyle: textStyle),
.foregroundColor: textColor ]
navigationBarAppearance.titleTextAttributes = textAttributes
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().compactAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
}
}
ViewController
:
import UIKit
class ViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = UIColor.lightGray
}
}
I'm not recommending you move back to the old navigation bar appearance API (e.g. barTintColor and friends). I'm just recommending you not call +appearance
to set the standardAppearance
and scrollEdgeAppearance
.
If you are running on iOS 13 or later, we absolutely recommend you only use the UINavigationBarAppearance
based APIs to customize your navigation bar (tintColor
is a UIView property, and so is fine to use as well). But you do not have to use them via the Appearance proxy (aka +[UIView appearance]
).
So all you need to do is change this:
if (@available(iOS 13.0, *))
{
[UINavigationBar appearance].standardAppearance.backgroundColor = color;
[UINavigationBar appearance].compactAppearance = [UINavigationBar appearance].standardAppearance;
[UINavigationBar appearance].scrollEdgeAppearance = [UINavigationBar appearance].standardAppearance;
}
to this:
if (@available(iOS 13.0, *))
{
navigationBar.standardAppearance.backgroundColor = color;
navigationBar.scrollEdgeAppearance = navigationBar.standardAppearance;
}
(it is rarely necessary to set the compactAppearance
because if it is not set we simply use standardAppearance
in its place).