Updating for dark mode: NSColor ignores appearance changes?
Asked Answered
V

5

13

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.

Velate answered 25/9, 2018 at 18:44 Comment(0)
L
25

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];
Leigha answered 26/9, 2018 at 11:38 Comment(10)
That works for system colors, but for colors from my asset catalog it always returns the light version.Velate
@Uncommon: It does work for asset colors in my project, so what are you doing with these colors? One thing you might want to try: NSColor * concrete = [NSColor colorWithCGColor:assetColor.CGColor]; (inside the appearance block I've described in the answer).Leigha
I convert the color to device RGB color space and get the RGB components so that I can pass it into the web view as a CSS RGB color.. I tried the CGColor round trip and it didn't help.Velate
@Uncommon: And you do that inside the snippet I posted in this answer? Try setting a breakpoint or add an NSLog and look at the effectiveAppearance you are setting to be the current appearance. Does that match what you are seeing on screen?Leigha
Yes, I do it inside a block like that, and the appearance name is always as expected. Code is here: github.com/Uncommon/Xit/blob/setAppearance/Xit/FileView/… ...with NSColor.cssRGB defined here: github.com/Uncommon/Xit/blob/setAppearance/Xit/Utils/…Velate
@Uncommon: I just checked out your repository. 1) There's crash since you do webView.window! in viewWillAppear since view is not added to window yet. 2) You should observe the effectiveAppearance of your (web)view, not its window. This also resolves the crash. 3) There's a lot of hard-coded colors in the generated HTML (right-click, inspect element; via BlameViewController); these won't change the way you are doing it right now and you need to think how to handle these. The text color does change, for example, since it's using CSS class that you are setting in updateColors.Leigha
Oh, and likewise in updateColors, assign the effectiveAppearance of your webView, not its window. These may be different.Leigha
Yeah, I just barely found that crash :) . I originally tried using the view's effectiveAppearance, but it wasn't working, so I used the window instead. Now I tried using the view again.. and it worked! No idea what made the difference this time. Thanks for your help, and especially for taking a thorough look at the code!Velate
@DarkDust, If you don't mind, updated your answer with documentation references? ✌️Bea
@IanBytchek: Thanks, I tried to further improve on that.Leigha
A
5

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.

Avie answered 12/9, 2020 at 10:55 Comment(2)
This generic function is a good idea. I would recommend to make it an extension of NSView and use the view's effectiveAppearance, though. Depending on the view hierarchy (vibrancy, overridden appearance) the appearance of a view may be different than the app's appearance.Leigha
How can I use it inside AppDelegate?Factory
C
4

As mentioned by @chrstphrchvz, NSAppearance.current is deprecated when using macOS 11 or newer. The new way to update anything with the latest appearance system settings works like this:

NSAppearance.currentDrawing().performAsCurrentDrawingAppearance {
    // Update your window or control or whatever
}
Counsel answered 10/12, 2022 at 10:52 Comment(0)
C
2

The currentAppearance property used in previous answers is now deprecated. The alternative as of macOS 11 Big Sur is to use the performAsCurrentDrawingAppearance: instance method.

Craal answered 14/9, 2022 at 17:16 Comment(0)
J
0
NSApplication.shared.effectiveAppearance.performAsCurrentDrawingAppearance {
    self.layer?.backgroundColor = colorCustom?.cgColor 
}
Jerilynjeritah answered 31/10, 2023 at 16:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.