@StateObject to Property Wrapper w SwiftUI do tworzenia i posiadania instancji ObservableObject bezpośrednio w widoku. SwiftUI gwarantuje, że obiekt jest inicjalizowany raz na cykl życia widoku i nie jest odtwarzany przy ponownym renderowaniu. Według Apple Developer Documentation (2025), @StateObject jest zalecany dla widoków głównych tworzących źródło danych. @StateObject to właściwy wybór do posiadania ObservableObject w hierarchii SwiftUI.
Najważniejsze
@StateObject to Property Wrapper, który pojawił się w SwiftUI 2.0 (iOS 14), łączący możliwości @ObservedObject i @State. Podobnie jak @ObservedObject, subskrybuje zmiany ObservableObject. Podobnie jak @State, gwarantuje, że dane przetrwają wielokrotne inicjalizacje struktury widoku. @StateObject tworzy obiekt raz przy pierwszym pojawieniu się widoku na ekranie i przechowuje go w stercie SwiftUI.
Przed pojawieniem się @StateObject programiści używali @ObservedObject dla wszystkich ObservableObject, w tym tworzonych w widokach. Prowadziło to do częstej utraty danych podczas aktualizacji widoku nadrzędnego, gdy struktura widoku była odtwarzana, zabierając ze sobą instancję @ObservedObject. @StateObject rozwiązał ten problem, dodając gwarancję stabilności.
Główna zasada: @StateObject stosuje się w widoku, który tworzy obiekt w domyślnym inicjalizatorze (let model = ViewModel()). Widoki potomne, które otrzymują ten obiekt, używają @ObservedObject. Taki podział gwarantuje jedyne źródło prawdy w całej hierarchii.
SwiftUI zarządza cyklem życia @StateObject przez menedżer przechowywania, analogiczny do @State. Przy pierwszym pojawieniu się widoku SwiftUI przydziela pamięć dla obiektu i zapisuje go w obszarze trwałym. Przy ponownym renderowaniu (wywołanie body) obiekt nie jest odtwarzany — używana jest istniejąca instancja. Obiekt żyje, dopóki widok znajduje się w hierarchii.
Gdy widok jest usuwany z hierarchii, SwiftUI niszczy @StateObject, wywołując deinit. Przy ponownym dodaniu widoku do hierarchii tworzona jest nowa instancja. Należy to uwzględnić przy projektowaniu: jeśli trzeba zachować dane między usunięciami widoku, użyj warstwy serwisowej (singleton lub DI) lub @AppStorage dla trwałości.
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() }
}
}
W przykładzie TimerViewModel jest tworzony przez @StateObject i żyje, dopóki TimerView jest na ekranie. Timer uruchamia się w onAppear i zatrzymuje w deinit. Gdyby użyto @ObservedObject, przy każdym renderowaniu TimerView tworzony byłby nowy TimerViewModel z sekundami = 0, a timer nigdy nie działałby poprawnie. @StateObject gwarantuje, że viewModel jest jedyny i stabilny.
Wybór między @StateObject a @ObservedObject zależy od tego, kto posiada obiekt. Jeśli widok tworzy obiekt — @StateObject. Jeśli widok otrzymuje gotowy obiekt — @ObservedObject. Ta zasada jest tak ważna, że Xcode wyświetla ostrzeżenie przy użyciu @StateObject w widoku potomnym, który otrzymuje obiekt przez inicjalizator.
| Sytuacja | Zalecany wrapper |
|---|---|
| Widok tworzy model przez ViewModel() | @StateObject |
| Widok otrzymuje model od rodzica | @ObservedObject |
| Model jest używany w jednym widoku | @StateObject |
| Model jest przekazywany przez Environment | @EnvironmentObject |
| Model jest potrzebny do podglądu | @ObservedObject + mock |
W praktyce na początku projektu często używa się @StateObject w widoku głównym i @ObservedObject we wszystkich potomnych. W miarę wzrostu aplikacji część @StateObject może być zastąpiona przez @EnvironmentObject w celu uproszczenia hierarchii. Jednak @StateObject pozostaje najlepszym wyborem dla modułowych ekranów z własną logiką.
Pierwszy wzorzec — MVVM z @StateObject. ViewModel jako ObservableObject jest tworzony w widoku przez @StateObject. ViewModel zawiera właściwości @Published i logikę biznesową. Widok subskrybuje zmiany i aktualizuje interfejs. Takie podejście zapewnia testowalną izolację: ViewModel można testować bez UI, tworząc instancję bezpośrednio.
Drugi wzorzec — @StateObject z zależnościami. Jeśli ViewModel wymaga serwisów, użyj inicjalizacji z parametrami. Na przykład @StateObject var viewModel = UserViewModel(api: APIClient.shared). Należy jednak uważać: parametry są obliczane przy każdym renderowaniu body, ale obiekt jest tworzony tylko raz. SwiftUI ignoruje kolejne inicjalizacje @StateObject.
Trzeci wzorzec — zagnieżdżone @StateObject. W SwiftUI można mieć wiele @StateObject w jednym widoku, ale rzadko jest to uzasadnione. Zazwyczaj jeden @StateObject odpowiada za cały zestaw danych widoku. Jeśli logika staje się zbyt skomplikowana, podziel ją na kompozycję @ObservedObject serwisów wewnątrz jednego @StateObject.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
W przykładzie AppView tworzy dwa @StateObject: NavigationRouter do zarządzania nawigacją i AuthViewModel do uwierzytelniania. Oba obiekty są wstrzykiwane do Environment przez environmentObject. Każdy widok potomny może uzyskać do nich dostęp przez @EnvironmentObject bez przekazywania przez łańcuch inicjalizatorów.
@StateObject obsługuje inicjalizację z dowolnymi parametrami, ale z ważną cechą: inicjalizator jest wywoływany tylko raz. Przy ponownym renderowaniu body nowa wartość parametrów jest ignorowana. Oznacza to, że jeśli przekażesz @State var id: Int = 5 do @StateObject var vm = ViewModel(id: id), to przy zmianie id ViewModel nie otrzyma nowej wartości.
Aby rozwiązać ten problem, użyj onReceive lub onAppear do synchronizacji. Subskrybuj zmiany parametru wewnątrz ViewModel przez Combine lub przekazuj parametry przez metodę .onChange(of:) na poziomie widoku. Alternatywą jest użycie @ObservedObject zamiast @StateObject, jeśli obiekt ma dynamicznie reagować na zmiany zewnętrzne.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
Prawidłowe podejście: DetailView otrzymuje itemId jako właściwość let (przekazywaną przez inicjalizator struktury), a @StateObject tworzy DetailViewModel bez parametrów. W onAppear wywoływana jest metoda load(id:), która ładuje dane dla przekazanego ID. Gwarantuje to, że ViewModel jest tworzony przez mechanizm @StateObject, ale dane są ładowane przy każdym pojawieniu się widoku z aktualnym ID.
Główny błąd — używanie @StateObject w widokach potomnych, które otrzymują obiekt od rodzica. Jeśli ParentView tworzy @StateObject model, a ChildView deklaruje @StateObject var model: ModelType (z parametrem domyślnym), to ChildView utworzy własną niezależną instancję. Obiekty rodzica i dziecka nie będą powiązane, a zmiany w jednym nie odzwierciedlą się w drugim.
Drugi błąd — umieszczanie @StateObject w List lub ForEach. Każdy element listy tworzy swój własny @StateObject, co prowadzi do wielu niezależnych instancji. Dla list poprawnie jest przekazać jeden ObservableObject wszystkim elementom przez @ObservedObject lub użyć struktur Identifiable z @State wewnątrz List.
Trzeci problem — brak czyszczenia w deinit. @StateObject żyje przez cały cykl życia widoku. Jeśli obiekt tworzy timery, subskrypcje Combine lub zapytania sieciowe, deinit powinien je anulować. W przeciwnym razie wycieki pamięci i kontynuacja pracy w tle po zamknięciu ekranu są nieuniknione. Zawsze używaj Cancellable store Combine lub invalidate timerów w deinit.
Często zadawane pytania
@StateObject został dodany w SwiftUI 2.0 na WWDC 2020 wraz z iOS 14, macOS 11, watchOS 7 i tvOS 14. Wcześniej @ObservedObject był jedynym sposobem pracy z ObservableObject, co prowadziło do częstych błędów z utratą danych.
Nie, @StateObject nie obsługuje typów Optional. Obiekt musi być zainicjalizowany przy deklaracji. Jeśli potrzebujesz opcjonalnego obiektu, użyj @ObservedObject lub @EnvironmentObject z typem opcjonalnym.
Dodaj print(#function) w inicjalizatorze i deinit ObservableObject. Jeśli przy ponownym renderowaniu init nie jest wywoływany — @StateObject działa poprawnie. Jeśli init jest wywoływany za każdym razem — zastąp @ObservedObject na @StateObject.
Tak, @StateObject działa w widokach SwiftUI osadzonych w UIKit przez UIHostingController. Cykl życia obiektu jest powiązany z widokiem SwiftUI, a nie z UIViewController. Jeśli widok SwiftUI jest zastępowany, @StateObject jest niszczony.
Kilka małych @StateObject z podzieloną odpowiedzialnością. Poprawia to testowalność, współużytkowanie i wydajność — przy zmianie jednego obiektu przerysowywane są tylko subskrybujące części interfejsu, a nie cały widok.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również