Понимание врапперов в Swift


Наряду со многими новыми функциями, которые появились в Swift 5.1, одна из самых интересных —  это врапперы свойств. По сути врапперы находятся между поведением свойств и их хранением. Врапперы свойств определяются с помощью struct, class, or enum. Также они могут применяться, если мы задаем свойства внутри этих типов.

Swift уже предоставлял несколько встроенных врапперов в предыдущих версиях, например lazy, @NSCopying, но с врапперами свойств разработчик может внедрять собственные без усложнения языка. О том, как это работает, можно прочесть в документации по ссылке.

Врапперы свойств активно используются в SwiftUI. Фреймворк предоставляет множество врапперов, например:

  • @State. Его значение привязывается к представлению, в котором оно объявлено.
  • @Binding. Предается вниз от родительского свойства State с использованием$ projectedValue.
  • @ObservedObject. Похож на@State, но используется для свойства, которое соотносится с протоколом ObservableObject. ObservableObject должен быть типом class и обновлять представление, когда изменяются свойства, помеченные @Published.
  • @Published. Этот враппер используется для свойств, заявленных в ObservableObject. Всякий раз, когда значение меняется, враппер вызывает метод objectWillChange, чтобы представление реагировало на изменения.
  • @EnvironmentObject. Похож на @ObservedObject, но может использоваться для обмена данными между различными представлениями сверху вниз по иерархии без передачи явного свойства дочернему представлению.
  • @Environment. Используется для внедрения и коррекции общесистемной конфигурации —  цветовой схемы системы, направления макета, размера содержимого — внутри представления.
  • Врапперы свойств не ограничены только SwiftUI. Со Swift 5.1 можно создавать пользовательские врапперы свойств! Вот, что можно делать , используя пользовательские врапперы:

  • Преобразовывать значение после того, как оно уже было назначено.
  • Задавать минимальные и максимальные границы значения.
  • Передавать свойству дополнительное значение.
  • Создавать враппер, ведущий себя как делегат, скрывающий детали реализации API.
  • Это всего лишь несколько примеров врапперов, которые можно создавать; возможности по сути безграничны! Давайте создадим несколько врапперов и посмотрим, как с их помощью можно упростить код.

    Использование врапперов свойств в двух словах

    Создать новый враппер очень просто:

  • Объявите ключевое слово @propertyWrapper до того, как объявить тип, в котором хотите использовать враппер свойства. Это может быть struct, class или enum.
  • Мы должны реализовать свойство wrappedValue. Обычно в этом свойстве объявляются пользовательские setter и getter. Это свойство может быть computed или stored.
  • При присвоении свойству значения при объявлении, блок инициализации передаст wrappedValue . Также можно создать пользовательский блок инициализации с дополнительными свойствами. Ниже в примерах с враппером @Ranged мы рассмотрим это подробнее. 
  • Можно задать дополнительное свойство projectedValue любого типа, с помощью префикса $ из свойства.
  • Чтобы использовать его, мы добавляем префиксом @ к врапперу, когда объявляем свойство в типе.

    Теперь давайте внедрим пользовательские врапперы! 

    Преобразование значения свойства 

    @propertyWrapper struct Uppercased { private var text: String var wrappedValue: String { get { text.uppercased() } set { text = newValue } } init(wrappedValue: String) { self.text = wrappedValue } } struct User { @Uppercased var username: String } let user = User(username: "alfianlo") print(user.username) // ALFIANLO

    Для этого @Uppercased враппера, мы хотим убедиться, что String всегда выводится в верхнем регистре, когда внутри свойства задано значение. Вот что нужно сделать для реализации: 

  • Сохраняем нужную строку внутри свойства, названного text. 
  • Необходимое wrappedValue свойство — вычисляемое. Всякий раз, когда мы задаем значение, оно будет храниться в свойстве text. Каждый раз при получении свойства, значение будет возвращено в верхнем регистре. 
  • Создаем блок инициализации wrappedValue и назначаем его свойству text при первой инициализации враппера. 
  • Чтобы запустить, просто добавляем ключевое слово @Uppercased перед свойством.
  • Обозначение минимальной и максимальной границ числового значения 

    @propertyWrapper struct Ranged<T: Comparable> { private var minimum: T private var maximum: T private var value: T var wrappedValue: T { get { value } set { if newValue > maximum { value = maximum } else if newValue < minimum { value = minimum } else { value = newValue } } } init(wrappedValue: T, minimum: T, maximum: T) { self.minimum = minimum self.maximum = maximum self.value = wrappedValue self.wrappedValue = wrappedValue } } struct Form { @Ranged(minimum: 17, maximum: 65) var age: Int = 0 } var form = Form() form.age = 100 // 65 form.age = 2 // 17

    @Ranged враппер можно использовать для фиксации значения числа, задав минимальную и максимальную границы значения. Каждый раз, когда присваивается значение, производится сравнение, и значение присваивается на основании следующих условий: 

  • Если новое присваиваемое значение больше максимальной границы, в свойстве сохраняется максимальное значение.
  • Если новое значение меньше минимальной границы, в свойстве сохраняется минимальное значение.
  • Если условия не пересекаются, в свойстве сохраняется новое значение. 
  • Чтобы принять минимальный и максимальный параметры, создается пользовательский блок инициализации. Когда мы объявляем свойство, нам также нужно передать значения максимума и минимума после объявления @Ranged.

    Свойство Project Date для отформатированной в ISO8601 строки

    @propertyWrapper struct ISO8601DateFormatted { static private let formatter = ISO8601DateFormatter() var projectedValue: String { ISO8601DateFormatted.formatter.string(from: wrappedValue) } var wrappedValue: Date } struct Form { @ISO8601DateFormatted var lastLoginAt: Date } let user = Form(lastLoginAt: Date()) print(user.$lastLoginAt) // "dd-mm-yyTHH:mm:ssZ"

    Врапперы свойств можно использовать, чтобы передавать другое значение любого типа, используя свойство projectedValue с префиксом $. Для ISO8601DateFormatter используется статичный private ISO8601DateFormatter. Каждый раз при чтении projectedValue преобразовывает дату из сохраненного свойства wrappedValue.

    Враппер свойства NSLocalizedString API 

    @propertyWrapper struct Localizable { private var key: String var wrappedValue: String { get { NSLocalizedString(key, comment: "") } set { key = newValue } } init(wrappedValue: String) { self.key = wrappedValue } } struct HomeViewModel { @Localizable var headerTitle: String @Localizable var headerSubtitle: String } let homeViewModel = HomeViewModel(headerTitle: "HOME_HEADER_TITLE", headerSubtitle: "HOME_HEADER_SUBTITLE") print(homeViewModel.headerTitle) // "Title" print(homeViewModel.headerSubtitle) // "Subtitle"

    Враппер свойства @Localizable используется для оборачивания NSLocalizedString API. Когда свойство объявлено с использованием ключевого слова @Localizable, значение будет сохранено в приватном свойстве key и будет использовано каждый раз, когда wrappedValue доступно при передаче NSLocalizedString(key:comment:) блоку инициализации для получения локализованной строки из приложения. 

    Оборачивание UserDefaults API 

    @propertyWrapper struct UserDefault<T> { var key: String var initialValue: T var wrappedValue: T { set { UserDefaults.standard.set(newValue, forKey: key) } get { UserDefaults.standard.object(forKey: key) as? T ?? initialValue } } } enum UserPreferences { @UserDefault(key: "isCheatModeEnabled", initialValue: false) static var isCheatModeEnabled: Bool @UserDefault(key: "highestScore", initialValue: 10000) static var highestScore: Int @UserDefault(key: "nickname", initialValue: "cloudstrife97") static var nickname: String } UserPreferences.isCheatModeEnabled = true UserPreferences.highestScore = 25000 UserPreferences.nickname = "squallleonhart"

    UserDefaults API может быть очень громоздким при сохранении и извлечении значений по умолчанию. Его можно упростить созданием враппера, который спрячет выполнение вызовов API.

    Враппер @UserDefault принимает 2 параметра в блоке инициализации, key и initialValue в том случае, если значение ключа не доступно в UserDefaults . Сам wrappedValue — это враппер-вычислитель. Он использует сохраненный key каждый раз, когда задано значение. При чтении свойства, key используется для получения значения generic. Если значение не доступно, вместо него вернется initialValue

    Заключение

    Врапперы свойств — великолепная функция, с помощью которой можно предоставлять пользовательские шаблоны и поведение настройкам, заданным в типе для упрощения кода. Я очень надеюсь, что в будущем сообщество будет создавать и распространять еще больше врапперов.

    Код на Github.


    Перевод статьи Alfian Losari: Understanding Property Wrappers in Swift By Examples


    Поделиться статьей:


    Вернуться к статьям

    Комментарии

      Ничего не найдено.