@StateObject — це Property Wrapper у SwiftUI для створення та володіння екземпляром ObservableObject безпосередньо в view. SwiftUI гарантує, що об'єкт ініціалізується один раз за життєвий цикл подання та не перестворюється при повторних рендерах. Згідно з Документацією розробника Apple (2025), @StateObject рекомендується для кореневих view, що створюють джерело даних. @StateObject — правильний вибір для володіння ObservableObject у ієрархії SwiftUI.
Головне
@StateObject — це Property Wrapper, який з'явився в SwiftUI 2.0 (iOS 14) та об'єднує можливості @ObservedObject і @State. Як і @ObservedObject, він підписується на зміни ObservableObject. Як і @State, він гарантує, що дані переживуть повторні ініціалізації структури view. @StateObject створює об'єкт один раз при першій появі view на екрані та зберігає його в купі SwiftUI.
До появи @StateObject розробники використовували @ObservedObject для всіх ObservableObjects, включаючи створені в view. Це призводило до частої втрати даних при оновленні батьківського view, коли структура view перестворювалася, забираючи з собою екземпляр @ObservedObject. @StateObject вирішив цю проблему, додавши гарантію стабільності.
Основне правило: @StateObject використовується в тому view, яке створює об'єкт у типовому ініціалізаторі (let model = ViewModel()). Дочірні view, які отримують цей об'єкт, використовують @ObservedObject. Це розділення гарантує єдине джерело істини в усій ієрархії.
SwiftUI керує життєвим циклом @StateObject через менеджер зберігання, аналогічний @State. При першій появі view SwiftUI виділяє пам'ять для об'єкта та зберігає його в персистентній області. При повторних рендерах (викликах body) об'єкт не перестворюється — використовується існуючий екземпляр. Об'єкт живе, доки view знаходиться в ієрархії.
Коли view видаляється з ієрархії, SwiftUI знищує @StateObject, викликаючи deinit. При повторному додаванні view в ієрархію створюється новий екземпляр. Це важливо враховувати при проектуванні: якщо потрібно зберегти дані між видаленнями view, використовуйте сервісний шар (сінглтон або DI) або @AppStorage для персистентності.
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
У прикладі TimerViewModel створюється через @StateObject та живе, доки TimerView на екрані. Таймер запускається в onAppear та зупиняється в deinit. Якби використовувався @ObservedObject, кожен рендер TimerView створював би новий TimerViewModel з seconds = 0, і таймер ніколи б не працював коректно. @StateObject гарантує, що viewModel є унікальним та стабільним.
Вибір між @StateObject та @ObservedObject залежить від того, хто володіє об'єктом. Якщо view створює об'єкт — @StateObject. Якщо view отримує готовий об'єкт — @ObservedObject. Це правило настільки важливе, що Xcode виводить попередження при використанні @StateObject в дочірньому view, яке отримує об'єкт через ініціалізатор.
| Ситуація | Рекомендований wrapper |
|---|---|
| View створює модель через ViewModel() | @StateObject |
| View отримує модель від батька | @ObservedObject |
| Модель використовується в одному view | @StateObject |
| Модель передається через Environment | @EnvironmentObject |
| Модель потрібна для попереднього перегляду | @ObservedObject + mock |
На практиці на початку проекту @StateObject часто використовується в кореневому view та @ObservedObject в усіх дочірніх view. З ростом програми деякі екземпляри @StateObject можуть бути замінені на @EnvironmentObject для спрощення ієрархії. Однак @StateObject залишається кращим вибором для модульних екранів з власною логікою.
Перший патерн — MVVM з @StateObject. ViewModel як ObservableObject створюється в view через @StateObject. ViewModel містить @Published-властивості та бізнес-логіку. View підписується на зміни та оновлює інтерфейс. Цей підхід забезпечує ізоляцію, що піддається тестуванню: ViewModel можна протестувати без UI, створивши екземпляр безпосередньо.
Другий патерн — @StateObject з залежностями. Якщо ViewModel потребує сервіси, використовуйте ініціалізацію з параметрами. Наприклад, @StateObject var viewModel = UserViewModel(api: APIClient.shared). Однак будьте обережні: параметри обчислюються при кожному рендері body, але об'єкт створюється лише один раз. SwiftUI ігнорує подальші ініціалізації @StateObject.
Третій патерн — вкладені @StateObject. У SwiftUI можна мати кілька @StateObject в одному view, але це рідко виправдано. Зазвичай один @StateObject відповідає за весь набір даних view. Якщо логіка стає занадто складною, розділіть її на композицію @ObservedObject-сервісів всередині одного @StateObject.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
У прикладі AppView створює два @StateObject: NavigationRouter для керування навігацією та AuthViewModel для аутентифікації. Обидва об'єкти впроваджуються в Environment через environmentObject. Будь-яке дочірнє view може отримати до них доступ через @EnvironmentObject без проходження через ланцюг ініціалізаторів.
@StateObject підтримує ініціалізацію з будь-якими параметрами, але з важливою особливістю: ініціалізатор викликається лише один раз. При повторних рендерах body нові значення параметрів ігноруються. Це означає, що якщо ви передаєте @State var id: Int = 5 в @StateObject var vm = ViewModel(id: id), то при зміні id ViewModel не отримає нового значення.
Для вирішення цієї проблеми використовуйте onReceive або onAppear для синхронізації. Підпишіться на зміни параметрів всередині ViewModel через Combine або передавайте параметри через метод .onChange(of:) на рівні view. Альтернатива — використання @ObservedObject замість @StateObject, якщо об'єкт повинен динамічно реагувати на зовнішні зміни.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
Правильний підхід: DetailView отримує itemId як let-властивість (передається через ініціалізатор структури), а @StateObject створює DetailViewModel без параметрів. В onAppear викликається метод load(id:), який завантажує дані для переданого ID. Це гарантує, що ViewModel створений механізмом @StateObject, але дані завантажуються при кожній появі view з актуальним ID.
Головна помилка — використання @StateObject в дочірніх view, які отримують об'єкт від батька. Якщо ParentView створює @StateObject model, а ChildView оголошує @StateObject var model: ModelType (з параметром за замовчуванням), то ChildView створить власний незалежний екземпляр. Батьківський та дочірні об'єкти не будуть пов'язані, і зміни в одному не відобразяться в іншому.
Друга помилка — розміщення @StateObject в List або ForEach. Кожен елемент списку створює власний @StateObject, що призводить до багатьох незалежних екземплярів. Для списків правильний підхід — передача одного ObservableObject всім елементам через @ObservedObject або використання Identifiable-структур з @State всередині List.
Третя проблема — відсутність очищення в deinit. @StateObject живе весь життєвий цикл view. Якщо об'єкт створює таймери, підписки Combine або мережеві запити, deinit повинен їх скасовувати. Інакше витік пам'яті та продовження фонової роботи після закриття екрану неминучі. Завжди використовуйте Combine Cancellable-сховище або інвалідуйте таймери в deinit.
Часто запитують
@StateObject був доданий в SwiftUI 2.0 на WWDC 2020 разом з iOS 14, macOS 11, watchOS 7 та tvOS 14. До цього @ObservedObject був єдиним способом роботи з ObservableObject, що часто призводило до помилок з втратою даних.
Ні, @StateObject не підтримує Optional-типи. Об'єкт повинен бути ініціалізований при оголошенні. Якщо потрібен опціональний об'єкт, використовуйте @ObservedObject або @EnvironmentObject з опціональним типом.
Додайте print(#function) в ініціалізатор та deinit ObservableObject. Якщо при повторних рендерах init не викликається — @StateObject працює коректно. Якщо init викликається кожного разу — замініть @ObservedObject на @StateObject.
Так, @StateObject працює в SwiftUI view, вбудованих в UIKit через UIHostingController. Життєвий цикл об'єкта прив'язаний до SwiftUI view, а не до UIViewController. Якщо SwiftUI view замінюється, @StateObject знищується.
Кілька маленьких @StateObjects з розділеною відповідальністю. Це поліпшує тестованість, повторне використання та продуктивність — при зміні одного об'єкта перемальовуються лише підписані частини інтерфейсу, а не весь view цілком.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.