iOS 13 — Is Your App Ready for the Dark?

Dark Theme is great for your eyes at night, a battery saver on an OLED screen, and besides all that, it really looks great in most of the apps. Using dark mode for your app is not difficult…

About this tutorial

Dark Theme is great for your eyes at night, a battery saver on an OLED screen, and besides all that, it really looks great in most of the apps.

Using dark mode for your app is not difficult, although it can take a few days depending on the number of UI elements you have and the number of screens.

To do this efficiently, you need to understand how this feature works and how to deal with tricky issues.

In this tutorial, I’ll explain how trait collection relates to interface styling, what dynamic colors and images are, and how to deal with backward compatibility.

It All Starts with Trait Collection

Apple introduced the traitCollection property back in iOS 8 to solve universal apps with iPad and iPhone devices.

The traitCollection property is part of UITraitEnviorment protocol, and this protocol is adopted by several classes — UIScreen, UIWindow, UIViewController, UIPresentationController, and UIView.

traitCollection contains the definition of the interface environment the object is living in: device type (iPad/iPhone), device size class (compact/regular), and display scale.

On iOS 13, Apple added something called userInterfaceStyle, which determine if the appearance should be light or dark.

if self.traitCollection.userInterfaceStyle == .dark {
// you are in the dark!
} else {
// your are in the light!
}

traitCollection configuration flows according to the interface hierarchy, starting from UIScreen to UIWindow and then UIViewControllers and UIViews.

So, as you can see, the definition of dark/light theme for every UI element is always inherited from its parent, but you can always override it yourself in case you want something different for a certain element.

For example, you can define a different appearance for a certain UIViewController, and this appearance will continue to flow to all its child view controllers and its UIViews.

Dynamic Colors

Up until now, UIColor contained only one color data. Starting from iOS 13, UIColor can be dynamic — meaning it can contain one set of RGB values for dark, and another one for light.

And this is the point when the dots connect — whenever users change to dark mode on their device settings, the UIScreen trait collection is changed, and all the UI objects underneath the hierarchy. If this hierarchy contains elements with color such as UILables and background colors, and their color is dynamic (contains both dark and light values), it will automatically change to the appropriate color.

As long as you keep everything dynamic, it will happen automatically. Sounds great, right?

How to Create a Dynamic Color?

Storyboard/interface builder

We have a few ways of doing that:

Storyboard/interface builder

Well, that’s pretty straightforward. Apple provided a set of predefined dynamic colors such as System Black Color, System Orange Color, etc. Each of them contains two versions of the color, one for dark and the other for light. After you set the color of a view in the storyboard, you don’t have to run the app in order to see how it looks — there’s a new option in xCode to switch the interface builder to dark mode. Pretty cool!

Asset catalog

In case you don’t know, there’s an option to create color sets on asset catalog, and you can use them later in both Code and Storyboard. When you create a new color set, choose the attributes inspector, and from the appearance popup, make sure the option “Any, Light, Dark” is selected. At this point, you can choose different colors for different styles.

Code

It is also possible to create dynamic color with code. Just initialize UIColor with a new initialize method — init(dynamicProvider: @escaping (UITraitCollection) ->UIColor) — and return the corresponding values according to the trait collection you get.

How to Respond to Appearance Changes

Whenever users change their appearance styles, all the view controllers and presentation controllers viewWillLayoutSubviews and containerWillLayoutSubviews methods are getting called (corresponding).

Also, UIView layoutSubviews and draw methods are called as well.

Hence, you can put any changes that you want to happen when the style is changed in those methods to be on the safe side.

Rather than that, since all those objects conform to UITraitEnviroment protocol, traitCollectionDidChange also gets called.

In UIView, tintColorDidChange() gets called as well.

Dynamic Images

Images, just like color sets, can also be dynamic. You can define different assets for light and dark styles, and it can be done directly from the assets catalog, the same way as with color sets.

When you select attributes inspector on the right pane, you’ll see the appearance popup menu. Just select “Any, Light, Dark,” and you will be requested to define the different assets for light, dark, and for each resolution and device (good luck with that).

For example, you can set sunrise for light mode, and sunset for dark mode.

UIImageView would take care of that automatically if it wasn’t clear till now.

Tricky Issues

Well, there are several kinds of stuff you should know about supporting the black theme.

If you initialize custom colors and images on your init and viewDidLoad methods, you should move them to your layoutSubviews() and viewDidLayoutSubiews() (correspondingly) so you can react to the theme changes, since we said that those methods are called again when traitCollection is changed.

Since CALayer is not part of UIKit, it will not respond to style changes, so this is something you need to take care of.

Sometimes you want to isolate the actual color you get from your dynamic color set. That can be very easy using the resolvedColor() method of UIColor.

let dynamicColor = UIColor.systemBackground
let traitCollection = view.traitCollection
let resolvedColor = dynamicColor.resolvedColor(with: traitCollection)

Activity Indicator also changed, and now it contains only two types — medium and large. However, the interface style will determine the color.

In attributed text, you’ll have to make sure you define a dynamic color for the text, otherwise the default color the system will give your label will be black, no matter what interface style the UIView traitCollection has, and black on black is, well, not really shown.

I Don’t Want Dark Mode

It’s very easy to disable it.

You can disable it for your whole app by setting UIUserInterfaceStyle to Light in your info.plist.

You can also disable it to a certain UIViewController/UIView (and its children) with the override overrideUserInterfaceStylevariable.

self.overrideUserInterfaceStyle = .dark // always dark

I Have an Existing App. Where Do I Start?

For a new app, with a deployment target of iOS 13, everything is very easy and smooth.

But if you have an existing app, that could be quite a challenge, especially if you already have some kind of skin/theme management already.

Supporting old versions is easy if you’re using interface builder and assets catalog to set colors and images. Just use dynamic colors, and for older versions, iOS will pick up the light version of the color.

If you intend to create dynamic colors using code, my suggestion is to wrap it with a function, and pass the two versions of the colors (dark and light) to that function. Inside that function, the version checks and initializes the color accordingly.

Also, in case you have a dark theme feature within your app, you should reconsider maintaining this feature, now that you have it out of the box.