@StateObject е Property Wrapper в SwiftUI за създаване и притежание на екземпляр ObservableObject директно в изгледа. SwiftUI гарантира, че обектът се инициализира веднъж по време на жизнения цикъл на изгледа и не се пресъздава при повторни рендерирания. Според Apple Developer Documentation (2025), @StateObject се препоръчва за коренови изгледи, които създават източник на данни. @StateObject е правилният избор за притежание на ObservableObject в йерархията на SwiftUI.
Основни точки
@StateObject е Property Wrapper, който се появява в SwiftUI 2.0 (iOS 14) и комбинира възможностите на @ObservedObject и @State. Като @ObservedObject, абонира се за промени на ObservableObject. Като @State, гарантира, че данните оцеляват многократни инициализации на структурата на изгледа. @StateObject създава обекта веднъж при първото появяване на изгледа на екрана и го съхранява в heap паметта на SwiftUI.
Преди появата на @StateObject, разработчиците използваха @ObservedObject за всички ObservableObject, включително създадените в изгледи. Това водеше до честа загуба на данни при актуализиране на родителския изглед, когато структурата на изгледа се пресъздаваше, взимайки със себе си и екземпляра @ObservedObject. @StateObject реши този проблем, добавяйки гаранция за стабилност.
Основното правило: @StateObject се прилага в изгледа, който създава обекта в инициализатора по подразбиране (let model = ViewModel()). Дъщерните изгледи, които получават този обект, използват @ObservedObject. Такова разделение гарантира единствен източник на истина в цялата йерархия.
SwiftUI управлява жизнения цикъл на @StateObject чрез мениджър за съхранение, аналогичен на @State. При първото появяване на изгледа, SwiftUI заделя памет за обекта и го записва в постоянна област. При повторни рендерирания (извикване на body), обектът не се пресъздава — използва се съществуващият екземпляр. Обектът живее, докато изгледът е в йерархията.
Когато изгледът се премахне от йерархията, SwiftUI унищожава @StateObject, извиквайки deinit. При повторно добавяне на изгледа към йерархията се създава нов екземпляр. Важно е да се вземе предвид при проектирането: ако трябва да запазите данни между премахвания на изгледи, използвайте сервизен слой (singleton или 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 е на екрана. Timer стартира в onAppear и спира в deinit. Ако беше използван @ObservedObject, при всяко рендериране на TimerView щеше да се създаде нов TimerViewModel с секунди = 0 и таймерът никога нямаше да работи правилно. @StateObject гарантира, че viewModel е единствен и стабилен.
Изборът между @StateObject и @ObservedObject зависи от това кой притежава обекта. Ако изгледът създава обекта — @StateObject. Ако изгледът получава готов обект — @ObservedObject. Това правило е толкова важно, че Xcode издава предупреждение при използване на @StateObject в дъщерен изглед, който получава обекта чрез инициализатор.
| Ситуация | Препоръчан wrapper |
|---|---|
| Изглед създава модел чрез ViewModel() | @StateObject |
| Изглед получава модел от родител | @ObservedObject |
| Модел се използва в един изглед | @StateObject |
| Модел се предава чрез Environment | @EnvironmentObject |
| Модел е необходим за преглед | @ObservedObject + mock |
На практика в началото на проекта често се използва @StateObject в кореновия изглед и @ObservedObject във всички дъщерни изгледи. С разрастването на приложението част от @StateObject може да бъде заменена с @EnvironmentObject за опростяване на йерархията. Въпреки това, @StateObject остава най-добрият избор за модулни екрани със собствена логика.
Първият модел — MVVM с @StateObject. ViewModel като ObservableObject се създава в изгледа чрез @StateObject. ViewModel съдържа @Published свойства и бизнес логика. Изгледът се абонира за промени и актуализира интерфейса. Този подход осигурява тестваема изолация: ViewModel може да бъде тестван без UI чрез директно създаване на екземпляр.
Вторият модел — @StateObject със зависимости. Ако ViewModel изисква услуги, използвайте инициализация с параметри. Например @StateObject var viewModel = UserViewModel(api: APIClient.shared). Бъдете внимателни: параметрите се изчисляват при всяко рендериране на body, но обектът се създава само веднъж. SwiftUI игнорира последващи инициализации на @StateObject.
Третият модел — вложени @StateObject. В SwiftUI можете да имате множество @StateObject в един изглед, но това рядко е оправдано. Обикновено един @StateObject отговаря за целия набор от данни на изгледа. Ако логиката стане твърде сложна, разделете я на композиция от @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. Всеки дъщерен изглед може да получи достъп до тях чрез @EnvironmentObject без предаване през веригата от инициализатори.
@StateObject поддържа инициализация с всякакви параметри, но с важна особеност: инициализаторът се извиква само веднъж. При повторни рендерирания на body новата стойност на параметрите се игнорира. Това означава, че ако предадете @State var id: Int = 5 на @StateObject var vm = ViewModel(id: id), при промяна на id ViewModel няма да получи новата стойност.
За решаване на този проблем използвайте onReceive или onAppear за синхронизация. Абонирайте се за промени на параметъра вътре в ViewModel чрез Combine или предавайте параметри чрез метода .onChange(of:) на ниво изглед. Алтернатива — използвайте @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, но данните се зареждат при всяко появяване на изгледа с актуалното ID.
Основната грешка — използване на @StateObject в дъщерни изгледи, които получават обекта от родителя. Ако ParentView създава @StateObject model, а ChildView декларира @StateObject var model: ModelType (с параметър по подразбиране), то ChildView ще създаде свой собствен независим екземпляр. Обектите на родителя и детето няма да бъдат свързани и промените в единия няма да се отразят в другия.
Втората грешка — поставяне на @StateObject в List или ForEach. Всеки елемент от списъка създава свой @StateObject, което води до множество независими екземпляри. За списъци е правилно да се предаде един ObservableObject на всички елементи чрез @ObservedObject или да се използват Identifiable структури с @State вътре в List.
Третият проблем — липса на почистване в deinit. @StateObject живее през целия жизнен цикъл на изгледа. Ако обектът създава таймери, Combine абонаменти или мрежови заявки, deinit трябва да ги отмени. В противен случай изтичане на памет и продължаване на фоновата работа след затваряне на екрана са неизбежни. Винаги използвайте Combine Cancellable store или invalidate на таймери в 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 изгледи, вградени в UIKit чрез UIHostingController. Жизненият цикъл на обекта е обвързан с SwiftUI изгледа, а не с UIViewController. Ако SwiftUI изгледът бъде заменен, @StateObject се унищожава.
Няколко малки @StateObject с разделена отговорност. Това подобрява тестваемостта, преизползването и производителността — при промяна на един обект само абонираните части на интерфейса се прерисуват, а не целият изглед.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също