Разработка
Темы в современных iOS-приложениях с UITraitCollection
Эта статья представляет собой законченное описание того, как добавить тематическое оформление в реальное приложение.
Совсем недавно (с iOS 17) Apple представила довольно удобный способ обработки смены тем в приложениях для iOS. Раньше это было довольно мучительно (об этом рассказывается в нескольких хорошо написанных статьях в блогах Кристиана Селига и Shadowfacts).
С появлением UITraitAppearance
все стало проще.
Эта статья представляет собой законченное описание того, как добавить тематическое оформление в реальное приложение. Я также включу пример кода для реального проекта, который вы можете скачать (или вы можете просто скачать его на GitHub прямо сейчас, если вы просто хотите погрузиться в работу).
Предварительные условия
Прежде чем использовать этот подход, необходимо помнить о двух вещах.
- Ваше приложение должно быть нацелено на iOS 17 или более новую версию.
- Вы должны использовать делегат сцены.
Поэтому, прежде чем продолжить, убедитесь, что все в порядке. В противном случае все не будет работать!
Теперь, когда мы разобрались с этим, давайте приступим.
Понимание коллекций трейтов
Коллекции трейтов — не совсем новинка в iOS; они появились еще в iOS 8. Так что если вы опытный iOS-разработчик, вы, вероятно, уже сталкивались с этим API.
Но для тех, кто еще не знаком с ним, вот как в документации UIKit определяется коллекция трейтов:
Коллекция данных, которая представляет собой среду для отдельного элемента пользовательского интерфейса вашего приложения.
Круто. Давайте читать дальше.
Теперь стало немного понятнее. UITraitCollection
представляет данные о том, как должны выглядеть и вести себя объекты, основанные на особенностях самого устройства и предпочтениях пользователя (хороший пример — темный режим).
Одна из проблем с UITraitCollection
заключается в том, что трейты неизменяемы. Как мы можем позволить пользователям менять темы, если это так? Здесь на помощь приходит протокол UIMutableTraits
(это одно из ключевых изменений в iOS 17, которое помогает сделать это возможным).
Вот что говорит Apple:
Немного жаргона, но это означает, что UIMutableTraits
позволяет нам манипулировать трейтами по требованию (кажется полезным 🤔).
Все, что нам нужно сделать, это привести наш трейт в соответствие с UITraitDefinition
, а затем добавить геттеры и сеттеры в расширение UIMutableTraits
.
Определяем вашу тему
У нас есть все необходимые компоненты, но как их использовать?
Давайте начнем с очень простой темы, у которой есть только два варианта (светлый и темный) и два свойства (цвет переднего плана и фона).
Мы воспользуемся динамическим инициализатором UIColor (init(dynamicProvider: @escaping (UITraitCollection) -> UIColor))
для определения наших цветов. Из документации:
Идеально. Как раз то, что нам нужно.
enum Theme: Int { case light case dark var name: String { switch self { case .light: return "Light" case .dark: return "Dark" } } static var backgroundColor: UIColor { return UIColor { traitCollection in switch traitCollection.theme { case .light: return .white case .dark: return .black } } } static var foregroundColor: UIColor { return UIColor { traitCollection in switch traitCollection.theme { case .light: return .black case .dark: return .white } } } }
Теперь создайте определение трейта.
Все трейты, содержащиеся в коллекции UITraitCollection
, соответствуют этому протоколу. Вы можете создавать собственные трейты, определяя свой собственный соответствующий тип.
// ... struct ThemeTrait: UITraitDefinition { typealias Value = Theme static let defaultValue = Theme.light static let affectsColorAppearance = true static var name: String = "theme" static var identifier = "com.company.theme" }
Наконец, расширим UITraitCollection
и добавим тему в UIMutableTraits
.
// ... extension UITraitCollection { var theme: Theme { self[ThemeTrait.self] } } extension UIMutableTraits { var theme: Theme { get { self[ThemeTrait.self] } set { self[ThemeTrait.self] = newValue } } }
Использование темы
Теперь, когда у нас есть тема, мы можем динамически применять значения темы к элементам пользовательского интерфейса. Это очень просто:
view.backgroundColor = Theme.backgroundColor
Но подождите, вы могли заметить, что мы кое-что упустили: тема по умолчанию явно будет работать, но как нам ее изменить? Для этого нам нужно использовать переопределения трейта.
Переопределения трейтов
Переопределение трейта — это способ изменить изменяемый трейт в коллекции трейтов (это та штука, которая была недавно представлена в iOS 17 и которая делает все это возможным). Вы, вероятно, захотите делать это при запуске приложения и при обновлении темы.
Чтобы обработать переопределение трейта при запуске приложения, его нужно будет включить в метод scene(_:willConnectTo:options:)
в делегате сцены (еще одно напоминание: подход, который мы используем, требует наличия сцен).
Вот как может выглядеть полная реализация делегата сцены:
import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let windowScene = scene as? UIWindowScene else { return } let window = UIWindow(windowScene: windowScene) let rootViewController = NavigationController(rootViewController: ItemListViewController()) window.rootViewController = rootViewController // Key line! Set the theme here ⬇️. windowScene.traitOverrides.theme = ThemeStore.shared.getTheme() self.window = window window.makeKeyAndVisible() } }
А вот соответствующая конфигурация, которая хранит тему. Возможно, у вас уже есть существующий метод сохранения настроек по умолчанию, поэтому не используйте этот код; это просто иллюстрация.
import UIKit struct ThemeStore { static let shared = ThemeStore() private let userDefaults: UserDefaults private let themeKey = "theme" private init(userDefaults: UserDefaults = .standard) { self.userDefaults = userDefaults } func setTheme(_ theme: Theme) { userDefaults.set(theme.rawValue, forKey: themeKey) } func getTheme() -> Theme { Theme(rawValue: userDefaults.integer(forKey: themeKey)) ?? .light } }
И наконец, вот как динамически изменить тему. Это можно сделать в любом месте, где есть доступ к windowScene
, например в UIViewController
:
let oldTheme = ThemeStore.shared.getTheme() let newTheme = oldTheme == .dark ? Theme.light : Theme.dark guard let windowScene = view.window?.windowScene else { return } // Update the current theme in the theme store and update the trait overrides. ThemeStore.shared.setTheme(newTheme) windowScene.traitOverrides.theme = newTheme
Заключение
Я только слегка прошелся по возможностям тематического оформления с помощью UITraitCollection API (черт, я даже не упомянул SwiftUI!). Но этого должно быть достаточно, чтобы начать.
Посмотрите пример приложения iOSTraitCollectionThemingExample на GitHub, демонстрирующий эту технику в действии.
Если вы хотите продолжить, вот несколько других ресурсов, которые я рекомендую:
- Theming iOS Apps is No Longer Hard, by Shadowfacts
- Dark Mode, from the iOS Human Interface Guidelines
- Adopting iOS Dark Mode
- WWDC23: Unleash the UIKit trait system
- WWDC19: Implementing Dark Mode on iOS
-
Видео и подкасты для разработчиков4 недели назад
SwiftUI: алхимия приложений — превращаем идеи в реальность
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.3
-
Магазины приложений2 недели назад
Приложение Hot Tub появится на iOS в EC
-
Разработка3 недели назад
Смешивание цветов в SwiftUI