Наряду со многими новыми функциями, которые появились в 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 можно создавать пользовательские врапперы свойств! Вот, что можно делать , используя пользовательские врапперы:
Это всего лишь несколько примеров врапперов, которые можно создавать; возможности по сути безграничны! Давайте создадим несколько врапперов и посмотрим, как с их помощью можно упростить код.
Создать новый враппер очень просто:
@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
всегда выводится в верхнем регистре, когда внутри свойства задано значение. Вот что нужно сделать для реализации:
@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
.
@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
.
@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:)
блоку инициализации для получения локализованной строки из приложения.
@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
Комментарии