In iOS 13.0 and later, users can choose to adopt a dark appearance called Dark Mode. In Dark Mode, apps and system use a darker colors for all screens, controls and views. Users can select Dark Mode as their default interface style, and can use Settings to make their devices automatically switch to Dark Mode when ambient light is low.

In this article I’ll describe Dark Mode support in third-party apps with(out) storyboards, highlight handy debugging tools and try to implement Dark Mode updating inside the app without overhead.

All my examples from this article are available in DarkMode project. This small framework is a result of my research and a place for experiments with Dark Mode. Please feel free to open it and play with examples.

Note: in this article I’ll tell about UIKit, not SwiftUI. The main goals of my research were practical use and backward compatibility.

Implementation

Dark mode appearance is based on trait collections. When the user changes the system appearance, the system automatically asks all windows and views to redraw its content. UIKit controls support it out of the box without additional logic. Let’s start with colors.

Color appearance

To configure app colors with different appearances, you can use Asset Catalogs. Just create a New Color Set and add required colors. If you want to improve app accessibility, you can add high contrast color variant for every color:

In storyboards these colors are available in Named Colors section during color selection. To use it in code, just initialize it with the given name:

let view = UIView()

view.backdroundColor = UIColor(named: "Color")

Note: I recommend that we use code generation tools for it to prevent silly crashes after renaming or refactoring.

If you don’t want to use Asset Catalog for some reason, you can configure colors directly via UIColor.init(dynamicProvider:) initializer. It returns different colors based on trait collection properties. I’ve added an extension to reduce SDK version checks:

By the way, iOS has some default colors that automatically adapt to the current trait environment:

let view = UIView()

view.backdroundColor = .systemRed

Image appearance

The same logic in Asset Catalog works for images as well:

Use it as usual in code:

let imageView = UIImageView()

imageView.image = UIImage(named: "Image")

If you want to create images at runtime, for example, load from file system or from server, you must use image assets. Also I’ve added an extension to initialize assets with two images for different appearances:

Layers

Previous examples work for views perfectly. The colors are updated automatically without additional logic. But what about layers?

To update colors in CALayer s, you should implement traitCollectionDidChange(_:) method in UIView or UIViewController and configure layer’s colors manually:

Pay attention to line #3. Trait collection may be changed for many reasons, for instance, when an iPhone is rotated from portrait to landscape orientation. This function indicates whether changing between the specified and current trait collections would affect color values. It saves you from extra drawing.

Debugging

There are many ways to test Dark Mode appearance in your apps. Let’s start with storyboards. If you use storyboards for layout, you can update interface style in the bottom next to device configuration pane.