Перейти к основному содержимому

Свойства-обёртки в Swift

Разработчику Архитектору

property wrapper - перехват доступа к свойству

В проектах часто повторяется одна и та же логика вокруг свойства: чтение/запись в UserDefaults, валидация, потокобезопасный доступ, синхронизация с UI. Копировать её в каждом поле утомительно и ошибочно.

Свойство-обёртка (property wrapper) — тип, который перехватывает доступ к помеченному свойству: при чтении и записи вызывается код обёртки, а снаружи синтаксис остаётся обычным (settings.isDarkMode = true).

Обёртки появились в Swift 5.1; в экосистеме Apple они стали основой SwiftUI (@State, @Binding, …) и части Combine.


Минимальная своя обёртка

@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>

init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}

var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}

struct Player {
@Clamped(0...100) var health = 100
}

Обязательные элементы типа-обёртки:

ЧленНазначение
wrappedValueТо, что видит код при обращении к свойству
init(wrappedValue:) или кастомный initНачальное значение и параметры обёртки
@propertyWrapper на типеПомечает тип как обёртку

Компилятор разворачивает @Clamped в хранение экземпляра обёртки и прокси-доступ к wrappedValue.


Параметры обёртки

Аргументы в скобках после @ передаются в инициализатор:

@Clamped(0...100) var stamina = 50

Можно задавать несколько параметров и значения по умолчанию — как у обычного инициализатора структуры.


projectedValue и $

Некоторые обёртки предоставляют проецируемое значение — доступ через префикс $ к дополнительному API (часто Binding в SwiftUI):

struct ContentView: View {
@State private var count = 0
var body: some View {
Stepper("Счёт: \(count)", value: $count)
}
}

count — обычное чтение/запись; $count — проекция для двусторонней связи с UI. Это не «магия доллара», а соглашение: если у обёртки есть projectedValue, компилятор генерирует $имя.

Подробнее про UI — фреймворки и жизненный цикл.


Пример без UI — настройки приложения

Повторяющийся доступ к UserDefaults удобно вынести в обёртку (упрощённый учебный вариант):

@propertyWrapper
struct Storage<T> {
let key: String
let defaultValue: T

var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}

struct AppSettings {
@Storage(key: "darkMode", defaultValue: false)
static var isDarkMode: Bool
}

В продакшене для сложных типов добавляют кодирование (Codable) и потокобезопасность; идея та же — одна реализация, много свойств.

Более развёрнутый пример — в рекомендациях по разработке.


Где встречаются в стандартной экосистеме

ОбёрткаКонтекст
@State, @Binding, @ObservedObjectSwiftUI — состояние и связь с UI
@PublishedCombine — уведомление об изменении
@MainActorИзоляция на главном акторе (async)
@UIApplicationDelegateAdaptor и др.Мосты в SwiftUI App

Перед SwiftUI достаточно понять модель: обёртка = тип + доступ к wrappedValue. Тогда аннотации Apple читаются как конкретные реализации той же идеи.


Ограничения и правила

  • Обёртку можно применять к stored property в struct, class, enum; для static и class — отдельные правила версии Swift.
  • Нельзя обернуть вычисляемое свойство без backing storage так же просто, как хранимое — компилятор требует место для экземпляра обёртки.
  • Обёртки на уровне локальных переменных и параметров поддерживаются в новых версиях языка — проверяйте целевую версию в Package.swift / настройках проекта.
  • Тестируйте обёртку отдельно: она должна вести себя предсказуемо при get/set.

Обёртки и расширения и макросы

ПодходКогда
Property wrapperПовторяющаяся логика доступа к одному свойству
extensionНовые методы/вычисляемые свойства для типа
Макросы (Swift 5.9+)Генерация кода на этапе компиляции, boilerplate типов

Исторический контекст эволюции — история Swift.


Практические советы

  1. Не оборачивайте всё подряд — обёртка оправдана при трёх и более повторениях одного паттерна.
  2. Документируйте, что делает get/set (потоки, персистентность, побочные эффекты).
  3. Имя обёртки должно читаться в объявлении: @ValidatedEmail var email понятнее @Wrapper1.
  4. Для SwiftUI сначала освойте @State и @Binding, затем @ObservedObject / @EnvironmentObject.

Связанные материалы


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).