@StateObject is a Property Wrapper in SwiftUI for creating and owning an ObservableObject instance directly in a view. SwiftUI guarantees that the object is initialized once per view’s lifecycle and is not recreated on subsequent re-renders. According to Apple Developer Documentation (2025), @StateObject is recommended for root views that create a data source. @StateObject is the right choice for owning an ObservableObject in the SwiftUI hierarchy.
Key Takeaways
@StateObject is a Property Wrapper introduced in SwiftUI 2.0 (iOS 14) that combines the capabilities of @ObservedObject and @State. Like @ObservedObject, it subscribes to ObservableObject changes. Like @State, it guarantees that data survives repeated view structure initializations. @StateObject creates the object once when the view first appears and stores it in SwiftUI’s heap.
Before @StateObject, developers used @ObservedObject for all ObservableObjects, including those created in views. This led to frequent data loss when the parent view updated, causing the view structure to be recreated and taking the @ObservedObject instance with it. @StateObject solved this by adding a stability guarantee.
The main rule: @StateObject is used in the view that creates the object in the default initializer (let model = ViewModel()). Child views that receive this object use @ObservedObject. This separation guarantees a single source of truth throughout the hierarchy.
SwiftUI manages the @StateObject lifecycle through a storage manager similar to @State. When the view first appears, SwiftUI allocates memory for the object and stores it in a persistent area. On subsequent re-renders (body calls), the object is not recreated — the existing instance is used. The object lives as long as the view is in the hierarchy.
When the view is removed from the hierarchy, SwiftUI destroys the @StateObject, calling deinit. When the view is added back to the hierarchy, a new instance is created. This is important to consider when designing: if you need to preserve data between view removals, use a service layer (singleton or DI) or @AppStorage for persistence.
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() }
}
}
In the example, TimerViewModel is created via @StateObject and lives as long as TimerView is on screen. The timer starts in onAppear and stops in deinit. If @ObservedObject were used, each re-render of TimerView would create a new TimerViewModel with seconds = 0, and the timer would never work correctly. @StateObject guarantees that the viewModel is unique and stable.
The choice between @StateObject and @ObservedObject depends on who owns the object. If the view creates the object — @StateObject. If the view receives a ready-made object — @ObservedObject. This rule is so important that Xcode shows a warning when using @StateObject in a child view that receives the object via an initializer.
| Situation | Recommended Wrapper |
|---|---|
| View creates model via ViewModel() | @StateObject |
| View receives model from parent | @ObservedObject |
| Model is used in one view | @StateObject |
| Model is passed via Environment | @EnvironmentObject |
| Model is needed for previews | @ObservedObject + mock |
In practice, at the start of a project, @StateObject is often used in the root view and @ObservedObject in all child views. As the application grows, some @StateObject instances can be replaced with @EnvironmentObject to simplify the hierarchy. However, @StateObject remains the best choice for modular screens with their own logic.
The first pattern — MVVM with @StateObject. ViewModel as ObservableObject is created in the view via @StateObject. ViewModel contains @Published properties and business logic. The view subscribes to changes and updates the interface. This approach provides testable isolation: ViewModel can be tested without UI by creating an instance directly.
The second pattern — @StateObject with dependencies. If the ViewModel requires services, use initialization with parameters. For example, @StateObject var viewModel = UserViewModel(api: APIClient.shared). However, be careful: parameters are computed on every body re-render, but the object is created only once. SwiftUI ignores subsequent @StateObject initializations.
The third pattern — nested @StateObjects. In SwiftUI, you can have multiple @StateObjects in one view, but this is rarely justified. Usually one @StateObject handles the entire view’s data set. If the logic becomes too complex, break it into a composition of @ObservedObject services inside one @StateObject.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
In the example, AppView creates two @StateObjects: NavigationRouter for managing navigation and AuthViewModel for authentication. Both objects are injected into the Environment via environmentObject. Any child view can access them through @EnvironmentObject without passing through the initializer chain.
@StateObject supports initialization with any parameters, but with an important caveat: the initializer is called only once. On subsequent body re-renders, new parameter values are ignored. This means that if you pass @State var id: Int = 5 into @StateObject var vm = ViewModel(id: id), when id changes, the ViewModel will not receive the new value.
To solve this problem, use onReceive or onAppear for synchronization. Subscribe to parameter changes inside the ViewModel via Combine or pass parameters through the .onChange(of:) method at the view level. An alternative is to use @ObservedObject instead of @StateObject if the object should dynamically respond to external changes.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
The correct approach: DetailView receives itemId as a let property (passed through the structure’s initializer), and @StateObject creates DetailViewModel without parameters. In onAppear, the load(id:) method is called to load data for the passed ID. This guarantees that the ViewModel is created by the @StateObject mechanism, but data is loaded on each view appearance with the current ID.
The main mistake — using @StateObject in child views that receive the object from a parent. If ParentView creates @StateObject model, and ChildView declares @StateObject var model: ModelType (with a default parameter), ChildView will create its own independent instance. The parent and child objects will not be connected, and changes in one will not reflect in the other.
The second mistake — placing @StateObject in List or ForEach. Each list element creates its own @StateObject, leading to multiple independent instances. For lists, the correct approach is to pass one ObservableObject to all elements via @ObservedObject or use Identifiable structures with @State inside List.
The third issue — lack of deinit cleanup. @StateObject lives for the entire view lifecycle. If the object creates timers, Combine subscriptions, or network requests, deinit must cancel them. Otherwise, memory leaks and continued background work after screen closure are inevitable. Always use a Combine Cancellable store or invalidate timers in deinit.
Frequently Asked Questions
@StateObject was added in SwiftUI 2.0 at WWDC 2020 alongside iOS 14, macOS 11, watchOS 7, and tvOS 14. Before that, @ObservedObject was the only way to work with ObservableObject, which often led to data loss bugs.
No, @StateObject does not support Optional types. The object must be initialized at declaration. If you need an optional object, use @ObservedObject or @EnvironmentObject with an optional type.
Add print(#function) to the ObservableObject’s initializer and deinit. If init is not called on re-renders — @StateObject is working correctly. If init is called every time — replace @ObservedObject with @StateObject.
Yes, @StateObject works in SwiftUI views embedded in UIKit via UIHostingController. The object lifecycle is tied to the SwiftUI view, not to the UIViewController. If the SwiftUI view is replaced, the @StateObject is destroyed.
Several small @StateObjects with separated responsibilities. This improves testability, reusability, and performance — when one object changes, only subscribed parts of the interface are redrawn, not the entire view.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also