Зачем нужны property wrapper-ы
Представьте, что вы пишете структуру Speaker с громкостью и в сеттере свойства
volume нужно всегда обрезать значение отрезком от 0 до 1: пользователь может передать
1.5 или -0.3, а хранить нужно только допустимое число. Ту же самую логику вы бы повторили для
яркости экрана, для процента скидки, для любого другого ограниченного значения — везде одинаковая
проверка границ внутри set. Копировать её по всем свойствам неудобно и легко забыть
хотя бы в одном месте.
Property wrapper решает именно эту задачу: логику хранения и проверки значения вы пишете один раз в отдельном типе, а потом применяете её к любому свойству одной строкой — атрибутом перед объявлением.
Как это называется в документации Apple
Тип, помеченный атрибутом @propertyWrapper, называется property wrapper
(«обёртка свойства»). Внутри него обязательно должно быть свойство wrappedValue —
именно оно хранит и отдаёт настоящее значение. Когда вы пишете @ИмяОбёртки var x = 5,
компилятор превращает это в создание экземпляра обёртки с этим значением, а все обращения к
x дальше по коду становятся обращениями к wrappedValue этой обёртки.
Простая обёртка
@propertyWrapper
struct Clamped01 {
private var value: Double
var wrappedValue: Double {
get { value }
set { value = min(1, max(0, newValue)) }
}
init(wrappedValue: Double) {
self.value = min(1, max(0, wrappedValue))
}
}
Здесь два места ограничивают значение: инициализатор — на случай, если стартовое значение уже
выходит за границы, и сеттер вычисляемого свойства wrappedValue — на каждое следующее
присваивание. Само хранимое значение спрятано в приватном value, наружу видна только
уже проверенная версия.
Применение обёртки к свойству
struct Speaker {
@Clamped01 var volume: Double = 0
}
var speaker = Speaker()
speaker.volume = 1.5
print(speaker.volume) // 1.0
Атрибут @Clamped01 перед var volume говорит: значением этого свойства
управляет обёртка Clamped01. Присваивание 0 при объявлении уходит в
init(wrappedValue:), а каждое следующее speaker.volume = ... — в сеттер
wrappedValue. Снаружи структуры Speaker никто не видит обёртку: код,
использующий speaker.volume, выглядит как обращение к обычному свойству типа
Double.
projectedValue: доступ через $
Обёртка может хранить не только основное значение, но и дополнительную информацию — например,
пришлось ли значение обрезать. Для этого у неё бывает второе свойство, projectedValue,
к которому обращаются через знак доллара перед именем свойства.
@propertyWrapper
struct ClampedReport {
private var value: Double
private(set) var wasClamped = false
var wrappedValue: Double {
get { value }
set {
let limited = min(1, max(0, newValue))
wasClamped = limited != newValue
value = limited
}
}
var projectedValue: Bool { wasClamped }
init(wrappedValue: Double) {
self.value = min(1, max(0, wrappedValue))
self.wasClamped = wrappedValue != self.value
}
}
Если бы у Speaker было свойство @ClampedReport var volume: Double, то
speaker.volume возвращало бы само число, а speaker.$volume —
Bool: пришлось ли его обрезать. Именно так устроены знакомые по чужому коду на
SwiftUI @State и @Published: @State — это обёртка над
значением экрана, которая через wrappedValue отдаёт само значение, а через
projectedValue ($name) — привязку для передачи в другие компоненты;
@Published устроен похоже, но публикует изменения значения наружу. В этом курсе
SwiftUI не изучается, но теперь строчки вида @State private var и
$someValue из чужих примеров перестанут быть магией: это тот же самый механизм
property wrapper.
Частые ошибки
- Забыть добавить
init(wrappedValue:), если у обёртки есть только другие параметры инициализации — тогда запись@Wrapper var x = 5не скомпилируется: компилятору нечем передать стартовое значение. - Ограничивать значение только в сеттере
wrappedValue, забыв про инициализатор — тогда стартовое значение, переданное при объявлении свойства, проходит без проверки. - Путать
wrappedValueи саму обёртку целиком: обращениеspeaker.volume— это всегдаwrappedValue, а не экземпляр структуры-обёртки. - Считать
@propertyWrapperчем-то из SwiftUI. Это обычная возможность языка Swift, SwiftUI просто активно её использует для своих задач.
Резюме
- Property wrapper — тип с атрибутом
@propertyWrapperи обязательным свойствомwrappedValue, который переносит логику хранения и проверки значения в одно место. - Атрибут
@ИмяОбёрткиперед свойством подключает эту логику одной строкой. - Стартовое значение при объявлении свойства уходит в
init(wrappedValue:)обёртки. projectedValue— необязательное второе значение обёртки, доступное через$имяСвойства.- На этом механизме построены
@Stateи@Publishedв SwiftUI.
Проверьте себя
3 вопроса
Ограничитель громкости
Реализуйте property wrapper Clamped01, который всегда хранит число типа Double в границах от 0 до 1 включительно: значения меньше 0 он должен заменять на 0, значения больше 1 — на 1. Обёртка уже применена к свойству volume структуры Speaker. Допишите функцию speakerVolume(_:) так, чтобы она создавала Speaker, присваивала свойству volume переданное число и возвращала итоговое значение после ограничения.