Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating for dark mode: NSColor ignores appearance changes?

In my web view, I'm using CSS variables to change various colors at runtime depending on whether macOS 10.14's dark mode is enabled. That much is working fine. The tricky part is updating the colors when the system appearance changes.

I'm detecting the change by observing the effectiveAppearance property on the window. That notification comes through as expected, but when I go to update the colors, NSColor still gives me the dark mode colors (or whichever mode the app started up in). For example, NSColor.textColor is is still white instead of black when I'm responding to a switch from dark mode to light. The same seems to happen with my own color assets.

Is there a different way or time that I should get these colors? Or could this be an OS bug?

Edit: I also tried creating a subclass of WebView and updating my colors in drawRect() if the name of the web view's effective appearance changes. The first time, I get all light colors, even when the app starts up in dark mode. After that, when I switch from light mode to dark, I get the dark versions of system colors and light versions of asset catalog colors.

Outside the debugger, switching to dark mode works, but the initial load always gets light colors.

like image 526
Uncommon Avatar asked Sep 25 '18 18:09

Uncommon


Video Answer


2 Answers

Changing the system appearance doesn't change the current appearance, which you can query and set and is independent from the system appearance. But the appearance actually depends on the "owning" view as within the same view hierarchy, several appearances may occur thanks to vibrancy and also manually setting the appearance property on a view.

Cocoa already updates the current appearance in a few situations, like in drawRect:, updateLayer, layout and updateConstraints. Everywhere else, you should do it like this:

NSAppearance * saved = [NSAppearance currentAppearance];
[NSAppearance setCurrentAppearance:someView.effectiveAppearance];

// Do your appearance-dependent work, like querying the CGColor from
// a dynamic NSColor or getting its RGB values.

[NSAppearance setCurrentAppearance:saved];
like image 138
DarkDust Avatar answered Sep 18 '22 20:09

DarkDust


And a Swifty version of the solution proposed by DarkDust:

extension NSAppearance {
    static func withAppAppearance<T>(_ closure: () throws -> T) rethrows -> T {
        let previousAppearance = NSAppearance.current
        NSAppearance.current = NSApp.effectiveAppearance
        defer {
            NSAppearance.current = previousAppearance
        }
        return try closure()
    }
}

that you can use with

NSAppearance.withAppAppearance {
    let bgColor = NSColor.windowBackgroundColor
    // ...
}

Note that I'm taking appearance from NSApp but it could be from a NSWindow or NSView.

like image 34
Ceylo Avatar answered Sep 21 '22 20:09

Ceylo