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

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

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

  1. @State. Его значение привязывается к представлению, в котором оно объявлено.
  2. @Binding. Предается вниз от родительского свойства State с использованием$ projectedValue.
  3. @ObservedObject. Похож на@State, но используется для свойства, которое соотносится с протоколом ObservableObject. ObservableObject должен быть типом class и обновлять представление, когда изменяются свойства, помеченные @Published.
  4. @Published. Этот враппер используется для свойств, заявленных в ObservableObject. Всякий раз, когда значение меняется, враппер вызывает метод objectWillChange, чтобы представление реагировало на изменения.
  5. @EnvironmentObject. Похож на @ObservedObject, но может использоваться для обмена данными между различными представлениями сверху вниз по иерархии без передачи явного свойства дочернему представлению.
  6. @Environment. Используется для внедрения и коррекции общесистемной конфигурации —  цветовой схемы системы, направления макета, размера содержимого — внутри представления.

Врапперы свойств не ограничены только SwiftUI. Со Swift 5.1 можно создавать пользовательские врапперы свойств! Вот, что можно делать , используя пользовательские врапперы:

  1. Преобразовывать значение после того, как оно уже было назначено.
  2. Задавать минимальные и максимальные границы значения.
  3. Передавать свойству дополнительное значение.
  4. Создавать враппер, ведущий себя как делегат, скрывающий детали реализации API.

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

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

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

  1. Объявите ключевое слово @propertyWrapper до того, как объявить тип, в котором хотите использовать враппер свойства. Это может быть struct, class или enum.
  2. Мы должны реализовать свойство wrappedValue. Обычно в этом свойстве объявляются пользовательские setter и getter. Это свойство может быть computed или stored.
  3. При присвоении свойству значения при объявлении, блок инициализации передаст wrappedValue . Также можно создать пользовательский блок инициализации с дополнительными свойствами. Ниже в примерах с враппером @Ranged мы рассмотрим это подробнее. 
  4. Можно задать дополнительное свойство 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 всегда выводится в верхнем регистре, когда внутри свойства задано значение. Вот что нужно сделать для реализации: 

  1. Сохраняем нужную строку внутри свойства, названного text. 
  2. Необходимое wrappedValue свойство — вычисляемое. Всякий раз, когда мы задаем значение, оно будет храниться в свойстве text. Каждый раз при получении свойства, значение будет возвращено в верхнем регистре. 
  3. Создаем блок инициализации wrappedValue и назначаем его свойству text при первой инициализации враппера. 
  4. Чтобы запустить, просто добавляем ключевое слово @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 враппер можно использовать для фиксации значения числа, задав минимальную и максимальную границы значения. Каждый раз, когда присваивается значение, производится сравнение, и значение присваивается на основании следующих условий: 

  1. Если новое присваиваемое значение больше максимальной границы, в свойстве сохраняется максимальное значение.
  2. Если новое значение меньше минимальной границы, в свойстве сохраняется минимальное значение.
  3. Если условия не пересекаются, в свойстве сохраняется новое значение. 

Чтобы принять минимальный и максимальный параметры, создается пользовательский блок инициализации. Когда мы объявляем свойство, нам также нужно передать значения максимума и минимума после объявления @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

Предыдущая статьяВозраст - это просто цифра
Следующая статьяФункциональное программирование в JavaScript: руководство с практическими примерами