Add a dark theme in iOS

Hello everyone!

My name is Andrey, I'm from the My Broker team. I will tell you ĸĸ added support for a dark theme in iOS.

Apple in iOS 13 added a dark theme for the entire system, users can choose a light or dark appearance on the iOS settings. In dark mode, the system uses a darker color palette for all screens, views, menus and controls.

image

Who cares - go under the cat.

Dark design support


The application created in Xcode 11 by default supports dark design in iOS 13. But for the full implementation of the dark mode, you need to make additional changes:

  • Colors should support light and dark design
  • Images should support light and dark design

Apple has added several system colors that support light and dark design.
image

In iOS 13, the new UIColor initializer was introduced :

init (dynamicProvider: @escaping (UITraitCollection) -> UIColor)

Add a static function to create color with support for switching between light and dark design:

extension UIColor {
    
    static func color(light: UIColor, dark: UIColor) -> UIColor {
        if #available(iOS 13, *) {
            return UIColor.init { traitCollection in
                return traitCollection.userInterfaceStyle == .dark ? dark : light
            }
        } else {
            return light
        }
    }
}

CGColor does not support automatic switching between light and dark. You must manually change the CGColor after changing the design.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
        
    layer.borderColor = UIColor.Pallete.black.cgColor
}

It is also possible to add color for dark decoration in resources.

image

But I prefer to add colors in the code.

UIColor.Pallete
extension UIColor {
    
    struct Pallete {

        static let white = UIColor.color(light: .white, dark: .black)
        static let black = UIColor.color(light: .black, dark: .white)

        static let background = UIColor.color(light: .white, dark: .hex("1b1b1d"))
        static let secondaryBackground = UIColor(named: "secondaryBackground") ?? .black

        static let gray = UIColor.color(light: .lightGray, dark: .hex("8e8e92"))

    }
}


For images, just add a variant of the image for a dark design right in the resources.

image

Let's make a small application for an example.


The application will contain two windows and three screens.

First window: authorization screen.

Second window: ribbon screen and user profile screen.

Screenshots in light and dark design
image image image
image image image

Switch light and dark themes


Create an enum for the topic:


enum Theme: Int, CaseIterable {
    case light = 0
    case dark
}

We add the ability to store the current theme in order to restore it after restarting the application.

extension Theme {
    
    //   UserDefaults
    @Persist(key: "app_theme", defaultValue: Theme.light.rawValue)
    private static var appTheme: Int
    
    //    UserDefaults
    func save() {
        Theme.appTheme = self.rawValue
    }
    
    //   
    static var current: Theme {
        Theme(rawValue: appTheme) ?? .light
    }
}

Persist
@propertyWrapper
struct Persist<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
    
    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }
}


To force the design, you need to change the style of all application windows.

We implement theme switching in the application.


extension Theme {
    
    @available(iOS 13.0, *)
    var userInterfaceStyle: UIUserInterfaceStyle {
        switch self {
        case .light: return .light
        case .dark: return .dark
        }
    }
    
    func setActive() {
        //   
        save()
        
        guard #available(iOS 13.0, *) else { return }
        
        //       
        UIApplication.shared.windows
            .forEach { $0.overrideUserInterfaceStyle = userInterfaceStyle }
    }
}

It is also necessary to change the window style to the current theme before showing the window.

extension UIWindow {
    
    //     
    //     
    func initTheme() {
        guard #available(iOS 13.0, *) else { return }
        
        overrideUserInterfaceStyle = Theme.current.userInterfaceStyle
    }
}

Screenshots of choosing a light or dark theme
image image

Add a switch to the system theme


Add a system theme to the enum theme.
enum Theme: Int, CaseIterable {
    case system = 0
    case light
    case dark
}

After the forced installation of a light or dark theme, it is impossible to determine which design is included in the system. To recognize the system design, we add a window to the application, in which we will not force the design change. It is also necessary to implement a design change when the application has a system theme installed and the user changes the design in iOS.

final class ThemeWindow: UIWindow {
    
    override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

        //         iOS,     .
        // :      .
        if Theme.current == .system {
            Theme.system.setActive()
        }
    }
}

let themeWindow = ThemeWindow()

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        ...
        //    ,    
        //       
        themeWindow.makeKey()
        ...
        return true
    }
}

extension Theme {
    
    @available(iOS 13.0, *)
    var userInterfaceStyle: UIUserInterfaceStyle {
        switch self {
        case .light: return .light
        case .dark: return .dark
        case .system: return themeWindow.traitCollection.userInterfaceStyle
        }
    }
    
    func setActive() {
        //   
        save()
        
        guard #available(iOS 13.0, *) else { return }
        
        //       
        //        
        UIApplication.shared.windows
            .filter { $0 != themeWindow } 
            .forEach { $0.overrideUserInterfaceStyle = userInterfaceStyle }
    }
}

Screenshots of choosing a system, light or dark theme
image image

Result


Support for dark design and switching between system, light and dark themes.
Screen video


Link to the whole project

All Articles