Property Wrapper — це механізм Swift, який додає шар логіки до доступу та зміни властивостей без дублювання коду. У SwiftUI Property Wrappers стали основою управління станом представлень: @State, @Binding, @ObservedObject, @StateObject та @Environment. За даними документації Swift (2025), обгортки властивостей скорочують шаблонний код у проектах у середньому на 40%. Розуміння Property Wrapper необхідне кожному iOS-розробнику для ефективної роботи з фреймворком.
Головне
Property Wrapper — це мовна конструкція Swift, що з'явилася у версії 5.1, яка дозволяє інкапсулювати логіку доступу до властивості в окремий тип. Замість того щоб писати повторювані геттери та сеттери в кожному класі, розробник оголошує обгортку один раз і застосовує її через анотацію @ перед типом. Swift автоматично обгортає властивість у вказаний тип, викликаючи його методи wrappedValue та projectedValue при читанні та записі. За даними Apple (WWDC 2019), Property Wrappers стали ключовою абстракцією для SwiftUI.
Обгортка властивості являє собою структуру або клас з атрибутом @propertyWrapper. Всередині такий тип зобов'язаний реалізувати властивість wrappedValue, яка повертає та встановлює актуальне значення. Компілятор Swift замінює звернення до оригінальної властивості на виклики wrappedValue, повністю приховуючи реалізацію від коду, що викликає. Додатково можна визначити projectedValue — проекцію, доступну через символ $.
Перевага Property Wrappers полягає в багаторазовому використанні логіки. Наприклад, можна створити обгортку для валідації email, кешування значень або синхронізації зі сховищем — і застосовувати її до будь-якої властивості в проекті. У SwiftUI ця концепція використовується повсюдно: кожен механізм управління станом реалізований як окремий Property Wrapper.
При оголошенні властивості з анотацією @WrapperType var value: T компілятор Swift трансформує код. Він створює екземпляр WrapperType і генерує доступ до властивості через wrappedValue. Вихідний код let x = value перетворюється на let x = _value.wrappedValue, а value = newValue — на _value.wrappedValue = newValue. Така трансформація відбувається на етапі компіляції, без runtime-оверхеду.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
У лістингу показано обгортку Capitalized, яка автоматично приводить рядок до формату з великою літерою. При присвоєнні значення сеттер викликає capitalized перед збереженням. Тепер будь-яка властивість з анотацією @Capitalized зберігатиме лише коректно відформатований текст. Такий підхід повністю виключає дублювання коду валідації та форматування.
Проекція (projectedValue) — додатковий канал зв'язку, доступний через префікс $. У SwiftUI ця можливість використовується повсюдно: $state дає Binding
SwiftUI включає п'ять вбудованих Property Wrappers для управління станом: @State, @Binding, @ObservedObject, @StateObject та @Environment. Кожен із них вирішує конкретну задачу та використовується в різних сценаріях. @State призначений для простих локальних даних, @Binding — для передачі посилання на дані в дочірні view, @ObservedObject та @StateObject — для складних об'єктів, @Environment — для системних значень з ієрархії.
| Wrapper | Призначення | Володіння |
|---|---|---|
| @State | Локальний стан одного view | Поточний view |
| @Binding | Двосторонній зв'язок із батьком | Батьківський view |
| @ObservedObject | Спостереження за зовнішнім об'єктом | Зовнішній власник |
| @StateObject | Створення ObservableObject | Поточний view |
| @Environment | Системні значення з ієрархії | Середовище SwiftUI |
Вибір конкретного Property Wrapper залежить від джерела даних та їхнього життєвого циклу. Якщо дані належать одному view і не потрібні дочірнім компонентам — використовуйте @State. Якщо дочірньому view потрібно змінювати дані батька — застосовуйте @Binding. Для об'єктів, які використовуються в кількох view, підходять @ObservedObject та @StateObject.
@State — це Property Wrapper для зберігання локального стану всередині одного view. SwiftUI автоматично керує пам'яттю для @State-властивостей і перемальовує view при кожній зміні. @State підходить для простих типів (String, Int, Bool, enum) та структур, які належать виключно поточному представленню. При зміні значення SwiftUI перезапускає властивість body.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Лічильник: \(count)")
Button("Збільшити") {
count += 1
}
}
}
}
У прикладі @State-властивість count зберігає поточне значення лічильника. SwiftUI створює область зберігання для цієї властивості в купі та пов'язує її з життєвим циклом CounterView. При натисканні кнопки count збільшується на 1, SwiftUI виявляє зміну та перезапускає body, відображаючи нове значення. Важливо: @State не слід використовувати для складних посилальних типів — для цього призначені @StateObject та @ObservedObject.
@Binding створює посилання на джерело даних, що належить іншому view. Binding не зберігає значення самостійно — він читає та записує дані через @State, @StateObject або інший Binding, переданий від батька. Це дозволяє дочірнім компонентам модифікувати стан предка без прямого володіння даними та без колбеків.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
У лістингу ToggleSwitch отримує @BindingBool від батьківського view. Батько створює @State var isToggleOn = false і передає $isToggleOn в ініціалізатор ToggleSwitch. Коли користувач перемикає Toggle всередині дочірнього view, зміна негайно відображається в батьківському @State. Механізм Binding повністю виключає необхідність у делегатах або замиканнях для передачі змін вгору по ієрархії.
@ObservedObject — це Property Wrapper для спостереження за екземпляром ObservableObject, переданим ззовні. View не володіє цим об'єктом — він створюється в батьківському компоненті або впроваджується через Environment. Коли будь-яка @Published властивість всередині ObservableObject змінюється, SwiftUI перемальовує всі view, підписані через @ObservedObject.
@StateObject — обгортка для створення та володіння ObservableObject безпосередньо у view. На відміну від @ObservedObject, @StateObject гарантує єдиний екземпляр об'єкта на весь життєвий цикл view. Навіть якщо SwiftUI перестворить структуру представлення (що відбувається часто), @StateObject збереже існуючий об'єкт і не викличе ініціалізатор повторно.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("Привіт, \(settings.username)")
}
}
У прикладі ProfileView створює UserSettings через @StateObject, стаючи власником об'єкта. ChildProfileView отримує той самий екземпляр через @ObservedObject — спостерігає, але не керує життєвим циклом. При зміні username обидва view оновлюються. Якби ChildProfileView використовував @StateObject замість @ObservedObject, при кожному рендері створювався б новий екземпляр із початковим значенням.
Ключове правило: @StateObject застосовується у view, яке створює об'єкт (джерело істини), а @ObservedObject — у view, яке отримує готовий об'єкт від батька. Порушення цього правила веде до втрати стану або неочікуваних перестворень даних.
Swift дозволяє створювати кастомні Property Wrappers для будь-якої повторюваної логіки доступу до властивостей. Достатньо оголосити структуру або клас з атрибутом @propertyWrapper та реалізувати wrappedValue. Нижче показана обгортка UserDefaultsWrapper, яка автоматично синхронізує значення з UserDefaults.
@propertyWrapper
struct UserDefaultsWrapper<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 AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
Обгортка UserDefaultsWrapper використовує дженерик T для роботи з будь-яким типом даних, що підтримується UserDefaults. Геттер читає значення за ключем, сеттер записує. Застосування @UserDefaultsWrapper(key:defaultValue:) до властивості theme автоматично пов'язує її зі сховищем — вся логіка роботи з UserDefaults захована всередині обгортки. Це типовий приклад зниження шаблонного коду за допомогою Property Wrappers.
При створенні власних обгорток важливо враховувати продуктивність. Оскільки геттер та сеттер викликаються при кожному доступі до властивості, не слід поміщати в wrappedValue важкі I/O-операції. Для асинхронного зберігання даних краще комбінувати Property Wrappers з ObservableObject та @Published.
Часті запитання
@State призначений для простих типів (String, Int, Bool) та структур, а @StateObject — для посилальних типів, що реалізують ObservableObject. @State зберігає значення безпосередньо в SwiftUI, @StateObject керує екземпляром класу в купі.
Так, @Binding можна створити з @StateObject, @ObservedObject або з іншого Binding за допомогою проекції $. Також Binding ініціалізується від ObservableObject через $object.$publishedProperty або від InlineBinding через Binding.constant(value).
Для глобальних даних використовуйте @EnvironmentObject або впроваджуйте ObservableObject через EnvironmentValues. @StateObject підходить для кореневого view з подальшою передачею через @ObservedObject дочірнім компонентам.
@ObservedObject не володіє об'єктом — якщо батьківське view перестворюється і передає новий екземпляр, @ObservedObject переключиться на нього. Щоб уникнути втрати стану, власне view повинно використовувати @StateObject.
Так, але простіше використовувати комбінацію ObservableObject з @Published та async-функціями всередині класу. Property Wrapper синхронний за своєю природою — wrappedValue обчислюється при кожному доступі, що не підходить для тривалих операцій.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також