Supporting Dark Mode: Checking Appearances

As you adapt your app to support Dark Mode, you may run into situations where you need to determine, in code, what the active appearance is. This is most typical in custom views that vary their drawing based on the current appearance, but you may also need to test higher level appearance traits to determine, for example, whether the whole application is being run “in Dark Mode” or not.

Current and Effective Appearances

To work effectively with Dark Mode support, you must appreciate the distinction between a few key properties that are all of type NSAppearance. Take some time to read the documentation and watch the videos I referenced in Educational Resources, so you really understand them. Here is a capsule summary to use as reference in the context of these articles:

  • appearance is a property of objects, such as NSApplication, NSWindow, and NSView, that implement the NSAppearanceCustomization protocol. Any such object can have an explicit appearance deliberately set on it, affecting the appearance of both itself and any objects that inherit appearance from it. As a rule, views inherit the appearance of their window, and windows inherit the appearance of the app.
  • effectiveAppearance is a property of those same objects, taking into account the inheritance hierarchy and returning a suitable appearance in the likely event that no explicit value has been set on the object.
  • NSAppearance.current or +[NSAppearance currentAppearance] is a class property of NSAppearance that describes the appearance that is currently in effect for the running thread. Practically speaking you can think of this property as an ephemeral drawing variable akin to the current fill color or stroke color. Its value impacts the manner in which drawing that is happening right now should be handled. Don’t confuse it with high-level user-facing options about which mode is set for the application as a whole.

High-Level Appearance Traits

As you modify your code to respect the current or effective appearance, you will probably need to make high level assessments like “is this appearance light or dark?” Because of the aforementioned complication that there are many types of NSAppearance, that they can be nested, etc., it’s not possible to simply compare the current appearance with a named appearance. Instead, you use a method on NSAppearance designed to evaluate which high-level appearance it is most like. If it’s a matter of simply distinguishing between light and dark appearances, you can use something like this:

let mode = NSAppearance.current
let isDark = mode.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua

In my applications, I found myself testing NSAppearance instances for lightness or darkness commonly enough that I implemented an “isDarkMode” property in an extension to NSAppearance:

// NSAppearance extension

@objc(rsIsDarkMode)
public var isDarkMode: Bool {
   let isDarkMode: Bool

   if #available(macOS 10.14, *) {
      if self.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
         isDarkMode = true
      } else {
         isDarkMode = false
      }
    } else {
        isDarkMode = false
    }

    return isDarkMode
}

As you can see this is laden with the requisite tests to ensure it works even if the app is running on a pre-10.14 Mac. A nice side-effect of centralizing this code in one place, is eliminating the need to include those checks at every call point.

This helper method is handy for working with NSAppearance instances, but in some contexts what I really want to know is bluntly: is this app running in dark mode or not? To accommodate this, I extend NSApplication to offer an identically named property:

// NSApplication extension

@objc(rsIsDarkMode)
public var isDarkMode: Bool {
   if #available(macOS 10.14, *) {
      return self.effectiveAppearance.isDarkMode
   } else {
      return false
   }
}

This method takes advantage of NSAppearance.isDarkMode, so it doesn’t have to replicate any of the important, but slightly clumsy “bestMatch” code either.

Having these convenient methods at my fingertips has been a great aid because it allows me to quickly answer the high-level question of whether an appearance is dark or not, regardless of whether I’m implementing drawing code or making a higher-level semantic interpretation of the application’s user-facing appearance.