Supporting Dark Mode: Responding to Change

To support Dark Mode elegantly in your app, you need to not only initialize your user interface appropriately for the current appearance, but also be prepared to adapt on the fly if the user changes appearance after your interface is already visible on the screen.

In situations where you use semantic colors or consult the current appearance at drawing time, there’s nothing else to be done. Since changing the appearance invalidates all views on the screen, your app will redraw correctly without any additional intervention.

Other situations may demand more customized behavior. I’ll give a couple examples later in this series, but for now just take my word that you may run into such scenarios.

If you have a custom view that needs to perform some action when its appearance changes, the most typical way to handle this is by implementing the “viewDidChangeEffectiveAppearance” method on NSView. This is called immediately after your view’s effectiveAppearance changes, so you can examine the new appearance, and update whatever internal state you need to before subsequent drawing or event handling methods are called:

class CustomView: NSView {
   override func viewDidChangeEffectiveAppearance() {
      // Update appearance related state here...
   }
}

If you need to react to appearance changes in the context of an NSViewController, your best bet may be to use Key Value Observing (KVO) to observe changes to the “effectiveAppearance” property on the view controller’s view. This approach will set your view controller up to be notified around the same time the view itself would be.

In some situations it’s more interesting to know whether, at a high level, the application’s appearance has shifted in a significant way. For this you can observe “effectiveAppearance” on the NSApplication.shared instance. There is no standard NotificationCenter notification for this, but I found it useful enough in my code bases that I added my own.

By centralizing KVO observation of NSApp.effectiveAppearance, I can translate all such changes into NotificationCenter-based broadcasts that any component of my app can observe. To implement something like this, first declare an NSNotification.Name extension in some high-level class such as your application delegate:

extension NSNotification.Name {
   public static let appAppearanceChanged =
      NSNotification.Name("appAppearanceChanged")
}

Then implement the centralized KVO somewhere early in launch, such as in applicationDidFinishLaunching:

observer = NSApp.observe(\.effectiveAppearance) { (app, _) in
   let nc = NotificationCenter.default
   nc.post(name: .appearanceChanged, object: app)
}

Now any code in your app that needs to be alerted when the app changes appearance can observe NSNotification.Name.appAppearanceChanged and respond by adjusting whatever it needs to.