iOS 13 Dark Mode
Tags: ios, swift, xcode, ui
Published: 2020-01-11 09:41:17

---
title: iOS 13 Dark Mode
published: true
description: Working with Dark Mode in iOS apps
tags: ios, swift, xcode, ui
cover_image: https://thepracticaldev.s3.amazonaws.com/i/osd87h4elfwio6km9pmc.jpeg
canonical_url: https://medium.com/rosberryapps/ios-13-dark-mode-29a1a601e301
---

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](https://github.com/rosberry/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:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/osd87h4elfwio6km9pmc.jpeg)

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:

```swift
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:

```swift
import UIKit

public extension UIColor {

    /// Creates a color object that generates its color data dynamically using the specified colors. For early SDKs creates light color.
    /// - Parameters:
    ///   - light: The color for light mode.
    ///   - dark: The color for dark mode.
    convenience init(light: UIColor, dark: UIColor) {
        if #available(iOS 13.0, tvOS 13.0, *) {
            self.init { traitCollection in
                if traitCollection.userInterfaceStyle == .dark {
                    return dark
                }
                return light
            }
        }
        else {
            self.init(cgColor: light.cgColor)
        }
    }
}
```

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

```swift
let view = UIView()
view.backdroundColor = .systemRed
```

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

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/9fyffsq9t676s287dtwk.png)

Use it as usual in code:

```swift
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:

```swift
import UIKit

public extension UIImageAsset {

    /// Creates an image asset with registration of tht eimages with the light and dark trait collections.
    /// - Parameters:
    ///   - lightModeImage: The image you want to register with the image asset with light user interface style.
    ///   - darkModeImage: The image you want to register with the image asset with dark user interface style.
    convenience init(lightModeImage: UIImage?, darkModeImage: UIImage?) {
        self.init()
        register(lightModeImage: lightModeImage, darkModeImage: darkModeImage)
    }

    /// Register an images with the light and dark trait collections respectively.
    /// - Parameters:
    ///   - lightModeImage: The image you want to register with the image asset with light user interface style.
    ///   - darkModeImage: The image you want to register with the image asset with dark user interface style.
    func register(lightModeImage: UIImage?, darkModeImage: UIImage?) {
        register(lightModeImage, for: .light)
        register(darkModeImage, for: .dark)
    }

    /// Register an image with the specified trait collection.
    /// - Parameters:
    ///   - image: The image you want to register with the image asset.
    ///   - traitCollection: The traits to associate with image.
    func register(_ image: UIImage?, for traitCollection: UITraitCollection) {
        guard let image = image else {
            return
        }
        register(image, with: traitCollection)
    }

    /// Returns the variant of the image that best matches the current trait collection. For early SDKs returns the image for light user interface style.
    func image() -> UIImage {
        if #available(iOS 13.0, tvOS 13.0, *) {
            image(with: .current)
        }
        return image(with: .light)
    }
}
```

### Layers

Previous examples work for views perfectly. The colors are updated automatically without additional logic. But what about layers?
To update colors in CALayers, you should implement `traitCollectionDidChange(_:)` method in UIView or UIViewController and configure layer's colors manually:

```swift
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    traitCollection.hasDifferentColorAppearance(comparedTo: traitCollection) {
        layer.backgroundColor = UIColor.layer.cgColor
    }
}
```

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.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/za6y8k2z07idh24msve4.png)

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

```swift
final class ViewController: UIViewController {
}

#if canImport(SwiftUI) && DEBUG

import SwiftUI

struct ViewControllerRepresentable: UIViewRepresentable {

    func makeUIView(context: Context) -> UIView {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let viewController = storyboard.instantiateViewController(withIdentifier: "ViewController")
        return viewController.view
    }

    func updateUIView(_ view: UIView, context: Context) {

    }
}

@available(iOS 13.0, *)
struct ViewController_Preview: PreviewProvider {

    static var previews: some View {
        Group {
            ViewControllerRepresentable()
                .colorScheme(.light)
                .previewDisplayName("Light Mode")
            ViewControllerRepresentable()
                .colorScheme(.dark)
                .previewDisplayName("Dark Mode")
        }
    }
}

#endif
```

Using `colorScheme(_:)` you can preview screens with both schemes simultaneously:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/vd7rzogd2ewalyn9eta1.png)

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

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/kqxqvcgrkgu3cu1iph6q.png)

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

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/5fxj1gm6qmqoc7i19c5h.png)

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`.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/r4qbpn7wmc9qglwhr897.png)

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

Unfortunately, iOS doesn't support updating color schemes dynamically for a single app. A simple way to implement it is saving selected `UIUserInterafaceStyle` and using it in next app sessions. A good option for this is UserDefaults:

```swift
public extension UserDefaults {

    var overridedUserInterfaceStyle: UIUserInterfaceStyle {
        get {
            UIUserInterfaceStyle(rawValue: integer(forKey: #function)) ?? .unspecified
        }
        set {
            set(newValue.rawValue, forKey: #function)
        }
    }
}
```

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

```swift
import UIKit

public extension UIApplication {

     func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
         if supportsMultipleScenes {
             for connectedScene in connectedScenes {
                 if let scene = connectedScene as? UIWindowScene {
                     for window in scene.windows {
                          window.overrideUserInterfaceStyle = userInterfaceStyle
                     }
                 }
             }
         }
         else {
             for window in windows {
                 window.overrideUserInterfaceStyle = userInterfaceStyle
             }
         }
     }
 }
```

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

At Rosberry we usually support two latest major iOS versions. At the time of writing this article it's iOS 12 and 13. The simplest way is to use only a light color scheme for iOS 12 devices.

## Conclusion

I've shared my experience as to the Dark Mode support. I hope this article will help you to update existing apps and to inspire you to develop new ones. I want to believe that in future Dark Mode will be default app feature without the need for additional adjustments for design and code.

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](https://github.com/rosberry/DarkMode) project to see how Dark Mode works.

### Apple

[WWDC 2019 - Implementing Dark Mode on iOS](https://developer.apple.com/videos/play/wwdc2019/214/)
[Human Interface Guidelines - Dark Mode](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/dark-mode/)
[Human Interface Guidelines - System Colors](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/#system-colors)
[Supporting Dark Mode in Your Interface](https://developer.apple.com/documentation/xcode/supporting_dark_mode_in_your_interface)

### Articles

[Dark Mode: Adding support to your app in Swift - SwiftLee](https://www.avanderlee.com/swift/dark-mode-support-ios/)
[Dark Mode on iOS 13 - NSHipster](https://nshipster.com.ru/dark-mode/)
[Adopting Dark Mode on iOS and Ensuring Backward Compatibility - Inside PSPDFKit](https://pspdfkit.com/blog/2019/adopting-dark-mode-on-ios/)

### Github

[aaronbrethorst/SemanticUI](https://github.com/aaronbrethorst/SemanticUI): iOS 13 Semantic UI: Dark Mode, Dynamic Type, and SF Symbols
[noahsark769/ColorCompatibility](https://github.com/noahsark769/ColorCompatibility): Use iOS 13+ system colors while defaulting to light colors on iOS <=12

### Plugins

[Color System Plugin for Sketch - Product Hunt](https://www.producthunt.com/posts/color-system-plugin-for-sketch)
[Lights - Light and Dark Mode - Figma](https://www.figma.com/c/plugin/780821534053786200/Lights---Light-and-Dark-Mode)