Разработка
Темы в современных 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
, соответствуют этому протоколу. Вы можете создавать собственные трейты, определяя свой собственный соответствующий тип.
xxxxxxxxxx
// ...
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
.
xxxxxxxxxx
// ...
extension UITraitCollection {
var theme: Theme { self[ThemeTrait.self] }
}
extension UIMutableTraits {
var theme: Theme {
get { self[ThemeTrait.self] }
set { self[ThemeTrait.self] = newValue }
}
}
Использование темы
Теперь, когда у нас есть тема, мы можем динамически применять значения темы к элементам пользовательского интерфейса. Это очень просто:
xxxxxxxxxx
view.backgroundColor = Theme.backgroundColor
Но подождите, вы могли заметить, что мы кое-что упустили: тема по умолчанию явно будет работать, но как нам ее изменить? Для этого нам нужно использовать переопределения трейта.
Переопределения трейтов
Переопределение трейта — это способ изменить изменяемый трейт в коллекции трейтов (это та штука, которая была недавно представлена в iOS 17 и которая делает все это возможным). Вы, вероятно, захотите делать это при запуске приложения и при обновлении темы.
Чтобы обработать переопределение трейта при запуске приложения, его нужно будет включить в метод scene(_:willConnectTo:options:)
в делегате сцены (еще одно напоминание: подход, который мы используем, требует наличия сцен).
Вот как может выглядеть полная реализация делегата сцены:
xxxxxxxxxx
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()
}
}
А вот соответствующая конфигурация, которая хранит тему. Возможно, у вас уже есть существующий метод сохранения настроек по умолчанию, поэтому не используйте этот код; это просто иллюстрация.
xxxxxxxxxx
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
:
xxxxxxxxxx
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 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков1 неделя назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8