iOS 13 Dark Mode

Artem Novichkov
Dec 30, 2019 · 6 min read

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

Color appearance

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

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

To update colors in CALayers, 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

One of the useful features of Xcode 11 is Xcode Preview. It’s possible to use it for UIKit-based projects with additional configuration:

UsingcolorScheme(_:) you can preview screens with both schemes simultaniously:

If you are debugging one of the color schemes, it’s handy to fix dark appearance in the Simulator in Preferences > Developer > Dark Appearance:

If it’s not enough for your case, you can override interface style during app sessions via Environment Overrides:

As I told earlier, trait collections may be changed a lot of times during app sessions. You can enable debug logging to easily see when traitCollectionDidChange(_:) is called in your own classes. Turn on the logging by using the following launch argument: -UITraitCollectionChangeLoggingEnabled YES.

When Dark Mode setting is updated, you’ll see a message in the Console like this:

2019-12-16 09:12:44.819195+0600 DarkModeExample[22611:3698294] [TraitCollectionChange] Sending -traitCollectionDidChange: to <DarkModeExample.ViewController: 0x7fdb81d0b7a0>► trait changes: { UserInterfaceStyle: Light → Dark }► previous: <UITraitCollection: 0x600002f98900; UserInterfaceIdiom = Phone, DisplayScale = 3, DisplayGamut = P3, HorizontalSizeClass = Compact, VerticalSizeClass = Regular, UserInterfaceStyle = Light, UserInterfaceLayoutDirection = LTR, ForceTouchCapability = Available, PreferredContentSizeCategory = L, AccessibilityContrast = Normal, UserInterfaceLevel = Base>► current: <UITraitCollection: 0x600002f98840; UserInterfaceIdiom = Phone, DisplayScale = 3, DisplayGamut = P3, HorizontalSizeClass = Compact, VerticalSizeClass = Regular, UserInterfaceStyle = Dark, UserInterfaceLayoutDirection = LTR, ForceTouchCapability = Available, PreferredContentSizeCategory = L, AccessibilityContrast = Normal, UserInterfaceLevel = Base>

Update Dark Mode dynamically

To apply the style saved we should override it for all app windows. They’re available via UIApplication.shared.windows. Pay attention that since iOS 13 iPad apps may support multiple windows

Don’t forget to override the style for windows created afterwards. After overriding it standard views and controls automatically update their appearance to match the current interface style.

Note: You may think about method swizzling to reduce configurations. I tried to avoid it in my example. Method swizzling is risky and may lead to unexpected behaviour. In my opinion for this case it’s a good decision.

What do you think? How did you implement this feature in your apps? And should apps have this option or should we rely on the system?

Backward compatibility

Conclusion

At the end I want to mention useful materials from around the web that helped me writing this article. And don’t forget to check DarkMode project to see how Dark Mode works.

Apple

WWDC 2019 — Implementing Dark Mode on iOS

Human Interface Guidelines — Dark Mode

Human Interface Guidelines — System Colors

Supporting Dark Mode in Your Interface

Articles

Dark Mode: Adding support to your app in Swift — SwiftLee

Dark Mode on iOS 13 — NSHipster

Adopting Dark Mode on iOS and Ensuring Backward Compatibility — Inside PSPDFKit

Github

aaronbrethorst/SemanticUI: iOS 13 Semantic UI: Dark Mode, Dynamic Type, and SF Symbols

noahsark769/ColorCompatibility: Use iOS 13+ system colors while defaulting to light colors on iOS <=12

Plugins

Color System Plugin for Sketch — Product Hunt

Lights — Light and Dark Mode — Figma

@RosberryApps

Mobile app design and development insights