Connect with us

Разработка

Темы в современных iOS-приложениях с UITraitCollection

Эта статья представляет собой законченное описание того, как добавить тематическое оформление в реальное приложение.

Опубликовано

/

     
     

Совсем недавно (с iOS 17) Apple представила довольно удобный способ обработки смены тем в приложениях для iOS. Раньше это было довольно мучительно (об этом рассказывается в нескольких хорошо написанных статьях в блогах Кристиана Селига и Shadowfacts).

С появлением UITraitAppearance все стало проще.

Эта статья представляет собой законченное описание того, как добавить тематическое оформление в реальное приложение. Я также включу пример кода для реального проекта, который вы можете скачать (или вы можете просто скачать его на GitHub прямо сейчас, если вы просто хотите погрузиться в работу).

Предварительные условия

Прежде чем использовать этот подход, необходимо помнить о двух вещах.

  • Ваше приложение должно быть нацелено на iOS 17 или более новую версию.
  • Вы должны использовать делегат сцены.

Поэтому, прежде чем продолжить, убедитесь, что все в порядке. В противном случае все не будет работать!

Теперь, когда мы разобрались с этим, давайте приступим.

Понимание коллекций трейтов

Коллекции трейтов — не совсем новинка в iOS; они появились еще в iOS 8. Так что если вы опытный iOS-разработчик, вы, вероятно, уже сталкивались с этим API.

Но для тех, кто еще не знаком с ним, вот как в документации UIKit определяется коллекция трейтов:

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

Круто. Давайте читать дальше.

Свойство traitCollection протокола UITraitEnvironment содержит характеристики (трейты), которые описывают состояние различных элементов пользовательского интерфейса iOS, таких как класс размера, масштаб отображения и направление компоновки. Вместе эти трейты составляют среду трейтов UIKit (trait environment).

Теперь стало немного понятнее. UITraitCollection представляет данные о том, как должны выглядеть и вести себя объекты, основанные на особенностях самого устройства и предпочтениях пользователя (хороший пример — темный режим).

Темы в современных iOS-приложениях с UITraitCollection

Одна из проблем с UITraitCollection заключается в том, что трейты неизменяемы. Как мы можем позволить пользователям менять темы, если это так? Здесь на помощь приходит протокол UIMutableTraits (это одно из ключевых изменений в iOS 17, которое помогает сделать это возможным).

Вот что говорит Apple:

Протокол UIMutableTraits предоставляет доступ на чтение и запись для получения и установки значений трейтов в базовом контейнере. UIKit использует этот протокол для облегчения работы с экземплярами UITraitCollection, которые являются неизменяемыми и доступны только для чтения. Инициализатор UITraitCollection init(mutations:) использует экземпляр UIMutableTraits, что позволяет установить пакет значений характеристик за один вызов метода. UITraitOverrides соответствует UIMutableTraits, что позволяет легко переопределять трейты в таких средах, как представления и контроллеры представлений.

Немного жаргона, но это означает, что 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, демонстрирующий эту технику в действии.

Если вы хотите продолжить, вот несколько других ресурсов, которые я рекомендую:

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: