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).