Добавляем темную тему в iOS

    Всем привет!

    Меня зовут Андрей, я из команды «Мой Брокер». Я рассĸажу Вам ĸаĸ добавлял поддержĸу темной темы в iOS.

    Apple в iOS 13 добавила темную тему для всей системы, пользователи могут выбрать светлое или темное оформление на настройках iOS. В темном режиме система использует более темную цветовую палитру для всех экранов, видов, меню и элементов управления.

    image

    Кому интересно — заходите под кат.

    Поддержка темного оформления


    Приложение созданное в Xcode 11 по-умолчанию поддерживает темное оформление в iOS 13. Но для полноценной реализации темного режима, необходимо внести дополнительные правки:

    • Цвета должны поддерживать светлое и темное оформление
    • Изображения должны поддерживать светлое и темное оформление

    Apple добавила несколько системных цветов, которые поддерживают светлое и темное оформление.
    image

    В iOS 13 был представлен новый инициализатор UIColor:

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

    Добавим статическую функцию для создания цвета с поддержкой переключения между светлым и темным оформлением:

    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 не поддерживает автоматическое переключение между светлым и темным оформлением. 
Необходимо вручную менять CGColor после изменения оформления.

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

    Так же есть возможность добавить цвет для темного оформления в ресурсах.

    image

    Но я предпочитаю добавлять цвета в коде.

    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"))
    
        }
    }


    Для изображений достаточно добавить вариант изображения для темного оформления прям в ресурсах.

    image

    Сделаем небольшое приложение для примера.


    Приложение будет содержать два окна и три экрана.

    Первое окно: экран авторизации.

    Второе окно: экран ленты и экран профиля пользователя.

    Скриншоты в светлом и темном оформлении
    image image image
    image image image

    Переключение светлой и темной темы


    Создаем enum для темы:


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

    Добавляем возможность хранения текущей темы, чтобы восстановить её после перезапуска приложения.

    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
        }
    }


    Чтобы принудительно установить оформление нужно изменить стиль всех окон приложения.

    Реализуем переключение темы в приложении.


    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 }
        }
    }

    Так же необходимо менять стиль окна на текущую тему перед показом окна.

    extension UIWindow {
        
        // Устанавливаем текущую тему для окна
        // Необходимо вызывать перед показом окна
        func initTheme() {
            guard #available(iOS 13.0, *) else { return }
            
            overrideUserInterfaceStyle = Theme.current.userInterfaceStyle
        }
    }

    Скриншоты выбора светлой или темной темы
    image image

    Добавляем переключение на системной тему


    Добавляем системную тему в enum темы.
    enum Theme: Int, CaseIterable {
        case system = 0
        case light
        case dark
    }

    После принудительной установки светлой или темной темы, нельзя определить какое оформление включено в системе. Чтобы узнавать системное оформление добавляем окно в приложение, у которого не будем принудительно менять оформление. Так же необходимо реализовать изменение оформления, когда в приложении установлена системная тема и пользователь меняет оформление в 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 }
        }
    }

    Скриншоты выбора системной, светлой или темной темы
    image image

    Результат


    Поддержка темного оформления и переключение между системной, светлой и темной темой.
    Скринвидео


    Ссылка на весь проект
    BCS FinTech
    Компания

    Комментарии 5

      0
      Зачем переключатель делать в приложение?
      Пользователь всегда может заменить тему в системных настройках.
      Почему цвета не вынесены в assets, там довольно легко настраивать цвета для разных оформления для тем.
        0
        Зачем переключатель делать в приложение?
        Пользователь всегда может заменить тему в системных настройках.

        Например некоторые пользователи хотят оставить светлую тему в iOS, а приложение сделать темным.

        Почему цвета не вынесены в assets, там довольно легко настраивать цвета для разных оформления для тем.

        Добавил в статью.
          0
          Например некоторые пользователи хотят оставить светлую тему в iOS, а приложение сделать темным.


          В каких приложениях есть такая «фича» -> чтобы пользователь «знал», что она там есть?

          Почему все-таки добавлять цвета в коде, а не в справочник?
            0
            В каких приложениях есть такая «фича» -> чтобы пользователь «знал», что она там есть?

            В ютубе, твиттере, mail почте да много где есть
            Странная претензия)
        0
        комментарий удалён

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое