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

15 ИЮНЯ 2022

------------------------------------------------------------------------

Наш телеграм-канал: t.me/@mailsgun_online

Youtube: youtube.com/@mailsgun_online

------------------------------------------------------------------------

Всем привет!

Меня зовут Андрей, я из команды «Мой Брокер». Я рассĸажу Вам ĸаĸ добавлял поддержĸу темной темы в 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

Результат

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

Скринвидео

Ссылка на весь проект

Source: habr1

Ну и напоследок напоминаю вам о нашем новом проекте vkserfing bot.