I have a dynamic color (let myColor = UIColor.label
) which is designed to appear differently in light and dark modes.
When I programmatically set dark theme, and then inquire a color's cgColor
components, it reports [1.0, 1.0]. It is correct, because in dark mode text color is expected to be white.
However, when I check it from within dispatch queue, it reports [0.0, 1.0], as if in light theme.
This is a minimal reproducing sample:
class ViewController: UIViewController {
let myColor: UIColor = .label
override func viewDidLoad() {
super.viewDidLoad()
UIApplication.shared.windows.forEach {
$0.overrideUserInterfaceStyle = .dark
}
if let color = myColor.cgColor.components {
print("initially \(color)")
DispatchQueue.main.async {
if let color = self.myColor.cgColor.components {
print("in queue \(color)")
}
}
}
}
}
Output:
initially [1.0, 1.0]
in queue [0.0, 1.0]
Any thoughts why it happens and how to fix it?
In UIKit, many commands, such as those that perform drawing and layout, do not take place immediately when the command is encountered; rather, they constitute a list of "things to do", which are actually performed later, namely, between screen refreshes at the end of the current CATransaction.
The phrase overrideUserInterfaceStyle = .dark
is a case in point. It does not elicit an immediate response: there is a delay, while the current CATransaction completes and all screen redraws take place, before the trait collection takes effect and trickles down to your view controller.
Therefore, your first print
takes place before the effective user interface style has actually changed to .dark
, and your second print
takes place after the effective user interface style has actually changed to .dark
. Basically, your second print
waits for the run loop to cycle once, after which the color change has taken place.
If you run your check like this, you will see that both print
statements give the same (expected) result:
CATransaction.setCompletionBlock {
if let color = self.myColor.cgColor.components {
print("initially \(color)")
DispatchQueue.main.async {
if let color = self.myColor.cgColor.components {
print("in queue \(color)")
}
}
}
}
UIApplication.shared.windows.forEach {
$0.overrideUserInterfaceStyle = .dark
}
Here, we don't perform any check until after the CATransaction completes, and so the first print
gets the same answer as the second print
.
You have observed that this answer, however, seems not to be the right answer. This is because CGColor extraction from a dynamic color such as label
does not automatically take place with regard to the current trait collection. You have to resolve the UIColor yourself. You can do this in various ways; you can say
self.myColor.resolvedColor(with: self.traitCollection).cgColor
or, perhaps more neatly, call performAsCurrent
and wrap that around your extraction of the CGColor:
CATransaction.setCompletionBlock {
self.traitCollection.performAsCurrent {
if let color = self.myColor.cgColor.components {
print("initially \(color)")
DispatchQueue.main.async {
self.traitCollection.performAsCurrent {
if let color = self.myColor.cgColor.components {
print("in queue \(color)")
}
}
}
}
}
}
(Sorry that this answer got rather long-winded, but it turned out that you really were asking about two mysteries: why were you getting two different answers, and why was the second answer "wrong".)