Paulo Andrade

Keeper of Secrets.

twitter github stackoverflow linkedin email
Adopting Dark Mode and Older Macs
Jun 13, 2018
3 minutes read

Update on 2019-08-06

Apple has added new API to NSColor on macOS Catalina (10.15) that provides the same functionality described on this post. If you’re able to, you should use that.

 let color = NSColor(name: "myColor") { appearance in 
 	switch appeareance.bestMatch(from: [.aqua, .darkAqua]) {
	case .darkAqua:
		return darkAquaColor
	case .aqua, default:
		return aquaColor
	}
 }

Update on 2019-11-02

Matt Masciotte wrote a small framework implementing this idea.

Mac OS Mojave was announced at this year’s WWDC, and with it Apple introduced Dark Mode — a new look for Apple’s apps and all third party apps willing to adopt it. And it looks great!

MacOS Dark Mode Finder Preview

MacOS Dark Mode Finder Preview

I believe that for any serious app on the Mac, adopting Dark Mode will be a must. And Secrets is no exception.

Apple had two sessions at WWDC just to cover Dark Mode. Succinctly, when it comes to colors, developers need to either:

  • use system colors declared on NSColor, such as labelColor, windowBackgroundColor, systemRed etc. These colors are will automatically change depending on wether the app is running in Dark Mode or not.
  • use asset catalog colors, which now can also vary based on Dark Mode. Colors in asset catalogs were introduced on macOS 10.13.
  • use a new API on NSAppearence to test for Dark Mode and “manually” change the colors. Like this:
     if (@available(macOS 10.14, *)) {
         NSAppearanceName appearanceName = [self.effectiveAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
         
         if ([appearanceName isEqualToString:NSAppearanceNameDarkAqua]) {
             // use dark color
         } else {
             // use light color
         }
     }
     

Overall this doesn’t sound to bad, but… if you want to use a color other than the dynamic colors offered by NSColor and are targeting a macOS older than 10.13, that last option is your only choice. And scattering the little dance above throughout your code is ugly as hell.

Fortunately, we can subclass NSColor and create our own dynamic colors.

The idea is simple, have a custom NSColor subclass that will respond as if it’s one color in Dark Mode and another when not. This will hide the logic of which color to use inside that class so the rest of your app does not need to be aware of any of this.

The subclass

Firstly, this subclass needs to know which colors to return in either mode:

@interface DynamicColor : NSColor

- (instancetype)initWithAquaColor:(NSColor *)aquaColor
      darkAquaColor:(NSColor *__nullable)darkAquaColor;

@end

To know which color to use at any given moment, we declare an effectiveColor method that returns the right object:

- (NSColor *)effectiveColor
{
    if (@available(macOS 10.14, *)) {
        NSAppearance *appearance = [NSAppearance currentAppearance] ?: [NSApp effectiveAppearance];
        
        NSAppearanceName appearanceName = [appearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
        
        if (self.darkAquaColor != nil && [appearanceName isEqualToString:NSAppearanceNameDarkAqua]) {
            return self.darkAquaColor;
        }
    }
    return self.aquaColor;
}

Finally, we just need to forward all of NSColor methods to the color instance returned by this method… which can be quite cumbersome. Fortunately, I was using Objective-C1 and a simple macro can give us a hand.

#define FORWARD( PROP, TYPE ) - (TYPE)PROP { return [self.effectiveColor PROP]; }

// which looks like this when used
FORWARD(colorSpace, NSColorSpace *)

I’ll spare you the rest of the forwarding methods, but you can get the entire class on this gist.

And that’s it!

Since Secrets already declared a bunch of colors in a separate style class, I was able to get a lot done simply by returning a DynamicColor instead of plain colors.

Note that the same caveats regarding dynamic colors declared by NSColor also apply here. If you’re extracting the CGColor to update a layer’s backgroundColor for example, you need to do it in one of the methods specified here. This is necessary because the extracted CGColor is not dynamic, and you also need to guarantee [NSAppearance currentAppearance] is returning the correct value (which it will inside those methods).


  1. Swift is cool… but not this cool ;) ↩︎



Back to posts