How to manually create dynamic (dark/light) NSImage instances?
Asked Answered
F

1

8

It seems in macOS 10.14 Mojave, the only way to create NSImage instances that automatically draw a light and dark version is via asset catalogs and +[NSImage imageNamed:]. However, I need to create dynamic images at runtime and there doesn't seem to be a way to do so without using private API.

Under the hood, it seems a private property _appearanceName has been introduced to NSImageRep that is somehow used to select the correct representation. It should be straight forward to create an NSImage with image representations that have the corresponding _appearanceName set but I would like to avoid this.

I found a simple workaround (posted below) but it doesn't seem to work correctly when the system appearance is changing (i.e. user is switching from light to dark or vice versa) or when used in view hierarchies that have the appearance property set to different appearances (e.g. one view hard-coded to dark mode, another view hard-coded to light mode).

So, how can I manually create a dynamic NSImage that is correctly showing a light or dark version, like the asset catalog images do?


@implementation NSImage (CustomDynamic)

+ (NSImage *)imageWithLight:(NSImage *)light dark:(NSImage *)dark
{
    if (@available(macOS 10.14, *)) {
        return [NSImage
            imageWithSize:light.size
            flipped:NO
            drawingHandler:^(NSRect dstRect) {
                if ([NSImage appearanceIsDarkMode:NSAppearance.currentAppearance]) {
                    [dark drawInRect:dstRect];
                } else {
                    [light drawInRect:dstRect];
                }
                return YES;
            }
        ];
    } else {
        return light;
    }
}

+ (BOOL)appearanceIsDarkMode:(NSAppearance *)appearance
{
    if (@available(macOS 10.14, *)) {
        NSAppearanceName basicAppearance = [appearance bestMatchFromAppearancesWithNames:@[
            NSAppearanceNameAqua,
            NSAppearanceNameDarkAqua
        ]];
        return [basicAppearance isEqualToString:NSAppearanceNameDarkAqua];

    } else {
        return NO;
    }
}

@end
Fabiano answered 17/10, 2018 at 7:22 Comment(0)
F
6

D'uh, it turned out the code posted in the question works just fine! The drawing handler is in fact called at appropriate times and does handle all the appearance situations.

However, I had code that scaled and cached those images and it was still using the ancient [image lockFocus]; … [image unlockFocus]; way of drawing images instead of using +[NSImage imageWithSize:flipped:drawingHandler:].

Fabiano answered 17/10, 2018 at 8:16 Comment(1)
It's ok, thanks for your code in Question I managed to handle it :)Gottfried

© 2022 - 2024 — McMap. All rights reserved.