132. Переключатель для поддержки темной темы
Возьмем рабочий проект, который поддерживает только светлую тему, и добавим туда возможность включить/выключить поддержку темной темы - не пикер темы, а именно переключатель для поддержки фичи.
Больше деталей
Есть iOS-приложение с жизненным циклом на UIKit
(AppDelegate
), которое никто раньше не адаптировал для темной темы.
В Info.plist
есть ключ UIUserInterfaceStyle
со значением Light
, что по умолчанию включает всегда светлую тему, даже если на девайсе активна темная тема.
Пользователи давно просят добавить поддержку темной темы, и пришло время это реализовать. Процесс займет неопределенное время, в ходе которого будет сделано несколько сборок для тестирования.
Чтобы было удобно переключаться между светлой и темной темами, добавим переключатель в дебаг-меню для тестовых сборок, куда нет доступа у пользователей - вот об этом и будет статья.
План
- Удаляем из
Info.plist
неактуальный ключUIUserInterfaceStyle
- Добавляем тоггл в дебаг-меню
- Складываем логику в отдельный сервис
- Дорабатываем
AppDelegate
Первые 2 пункта не очень интересные: удалить ключ из Info.plist
очень легко, а добавить тоггл в дебаг-меню - по-разному, в зависимости от проекта.
В нашем случае добавить тоггл было легко, поэтому перехожу к третьему шагу.
Складываем логику в отдельный сервис
Управление поддержкой темной темы - это новая фича, которую хочется покрыть тестами, поэтому сделаем небольшой сервис, который будет заниматься этой логикой, и позже напишем тесты.
Для управления поддержкой темной темы нам нужна ссылка на window
, которая в нашем случае есть в AppDelegate
, и значение тоггла из дебаг-меню:
import UIKit enum DarkModeSwitcher { @discardableResult static func applyIfNeeded(window: UIWindow?, isDarkModeEnabled: Bool) -> Bool { guard let window else { return false } let desired: UIUserInterfaceStyle = isDarkModeEnabled ? .unspecified : .light guard window.overrideUserInterfaceStyle != desired else { return false } window.overrideUserInterfaceStyle = desired return true } }
- Включение
darkMode
включает.unspecified
, если ранее было.light
и возвращаетtrue
- Отключение
darkMode
устанавливает.light
, если ранее было.unspecified
и возвращаетtrue
- Повторный вызов с тем же значением не меняет стиль и возвращает
false
- Вызов с
nil
окном безопасен и возвращаетfalse
Дорабатываем AppDelegate
Первым делом дорабатываем метод application(_:didFinishLaunchingWithOptions:), который вызывается при запуске приложения:
func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // ... DarkModeSwitcher.applyIfNeeded( window: window, isDarkModeEnabled: isDarkModeEnabled // <- ссылка на тоггл в дебаг-меню ) // ... return true }
Чтобы тестировать было удобнее, вызовем DarkModeSwitcher
еще и при "разворачивании" приложения, т.е. когда оно переходит в активное состояние - таким образом можно будет сразу после переключения тоггла в дебаг-меню свернуть и развернуть приложение и сразу же увидеть темную тему (если она активна на устройстве):
func applicationDidBecomeActive(_: UIApplication) { DarkModeSwitcher.applyIfNeeded( window: window, isDarkModeEnabled: isDarkModeEnabled ) }
Готово! Теперь можно продолжать процесс переезда на темную тему и не волноваться о том, что пользователи могут увидеть промежуточный результат, ну и не "солить" код в отдельной ветке.
Бонус: SwiftUI
Как уже можно было догадаться, если жизненный цикл вашего приложения использует SwiftUI, то достаточно модификатора preferredColorScheme
- в него нужно передать значение тоггла из дебаг-меню и ... готово!
Заключение
Код сервиса-переключателя для темной темы вместе с тестами можно посмотреть в гитхабе. Если интересно почитать про план адаптации темной темы в этом приложении с нуля, ставьте лайк 👍