iosswiftuikitdarkmodecgcolor

CGColor does not behave correctly in dark mode when used in dispatch queue


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?


Solution

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