Paint It Black: Dark Mode in macOS Mojave

Starting with macOS Mojave, users can choose to run a system-wide light or dark mode of the Aqua interface. While opting an application into light and/or dark appearance modes is ultimately the choice of the developer, users may come to expect that apps support both. Apple believes some content creation applications may make sense primarily as dark mode only applications.

Overview

Standard views and controls are ready for Dark Mode out-of-the-box. And if you’re already using asset catalogs and semantic colors, you’ll be able to make your app ready too with minimal code changes.

Our Astronomical app was designed using just the Aqua interface but we would like to support Dark Mode as well. We first built the app using the macOS 10.14 SDK, but it seems like we have some work to do.

Our header text was previously hardcoded to NSColor.black but we can easily achieve the same result using the dynamic .headerTextColor which will now do the right thing when we switch appearances.

Next, we would like to change our sun asset to a moon to represent dark mode. This can be achieved using the Asset Catalog. The Asset Catalog provides a convenient way to return a different resource using the same name that varies based on device attributes. In our case, the device attribute that is changing is the Appearance.

Note: If you are still targeting 10.13 or earlier, trying to receive a named image asset will only return the “Any Appearance” versions.

Detecting Appearance Changes Programmatically

There are some cases where you may want to respond to appearance changes programmatically. You may have already noticed that Xcode 10 is nice enough to remember your Source Editor theme preference for each appearance. We would like to use this trigger to update our emoji representation of the Astronomical object we are presenting on screen.

How can we do something like this in our code? We can detect appearance changes by observing effectiveAppearance from the newly formalized NSAppearanceCustomization protocol. This protocol has informally existed for a while, but is now adopted by NSPopover, NSView, NSWindow, and NSApplication (new as of 10.14).

Once you detect an appearance change, you will want to figure out which appearance the application is currently in. Switching on effectiveAppearance.name will work, but switching on bestMatch(from:) may be a more future-proof approach. This method will attempt to provide you a good match based on the appearances you provide as the parameter.

Update Custom Views Using Established Methods

While our Astronomical app didn’t need to override the methods below, it is worth mentioning that these methods are the ideal place to update views when appearances change. Depending on your implementation, some of these NSView methods will be called when an appearance changes:

updateLayer()

draw(_:)

layout()

updateConstraints()

You can trigger these methods manually by setting needsLayout, needsDisplay, and needsUpdateConstraints.