Mastering SwiftUI @StateObject: Persisting View Models Across Reloads
Learn how SwiftUI’s @StateObject keeps a single view model instance across view reloads, how to inject it into child views, and when not to use it.
30 Aug 2025, 17:29 UTC

Concrete Problem: State Vanishes on View Reloads
When you build a SwiftUI app that fetches data asynchronously, you often declare an ObservableObject and bind it to a view. If you use @ObservedObject or a plain State wrapper, the instance is recreated every time the view body is recomputed. That means network calls repeat, timers restart, and the UI can flash empty data. The question is: how do we keep a single, long‑lived view model that survives view reloads?
Thesis: @StateObject Owns the View Model
The @StateObject property wrapper was introduced in iOS 14 to solve exactly this problem. It stores a reference‑type ObservableObject that is owned by the view that declares it. When SwiftUI rebuilds the view body, the wrapper guarantees that the same instance is reused, so the state and any Combine publishers survive.
Key Benefits
- Single source of truth across the view hierarchy.
- Automatic cancellation of Combine subscriptions when the view is deallocated.
- No need for manual deinitialization or manual cleanup code.
Section 1: Declaring a @StateObject
Place the wrapper on a top‑level view that owns the lifecycle. Below is a minimal view model that fetches a temperature value asynchronously.
import SwiftUI
import Combine
class WeatherViewModel: ObservableObject {
@Published var temperature: Double?
private var cancellable: AnyCancellable?
init() {
fetchTemperature()
}
func fetchTemperature() {
// Simulated async fetch
cancellable = Just(72.5)
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.sink { [weak self] temp in
self?.temperature = temp
}
}
deinit {
print("WeatherViewModel deinitialized")
}
}
Notice the deinit print; you can use this in a real app to confirm that the object is released when the parent view disappears.
Section 2: Injecting into Child Views
Once the parent owns the model, you can pass it down to children either as a regular parameter or expose it via @EnvironmentObject if many descendants need it.
struct ContentView: View {
@StateObject private var viewModel = WeatherViewModel()
var body: some View {
VStack {
if let temp = viewModel.temperature {
Text("Temperature: \(temp)°F")
} else {
ProgressView()
}
ChildView(viewModel: viewModel)
}
}
}
struct ChildView: View {
@ObservedObject var viewModel: WeatherViewModel
var body: some View {
Button("Refresh") {
viewModel.fetchTemperature()
}
}
}
Because viewModel is a reference type, both ContentView and ChildView see the same instance. Updating temperature in either view immediately reflects in the other.
Section 3: Automatic Cleanup
When ContentView is removed from the view hierarchy, SwiftUI automatically calls deinit on its @StateObject property. Any Combine publishers attached to the object are cancelled, freeing resources without extra code. This is especially handy for long‑running network streams or timers.
Trade‑offs & Limitations
- Placement matters: Don’t declare
@StateObjectinside aListrow or a view that is recreated frequently; each recreation would create a new instance, defeating the purpose. - Not for shared parents: If several sibling views need the same model but are not in a parent–child relationship, use
@ObservedObjector@EnvironmentObjectinstead to avoid duplicate instances. - Availability:
@StateObjectis only available on iOS 14+, macOS 11+, watchOS 7+, tvOS 14+. Guard with@availableor provide a fallback for older OS versions. - Mixing wrappers: Avoid using
@StateObjectand@ObservedObjecton the same type in the same view hierarchy; it can lead to subtle bugs where updates are not propagated.
Actionable Take‑aways
- Declare
@StateObjecton the top‑level view that owns the data you want to persist across reloads. - Pass the instance to children via parameters or
@EnvironmentObjectwhen many views need it. - Use
@ObservedObjectfor view models that are created elsewhere and simply observed. - Wrap
@StateObjectusage in@availablechecks if you need to support iOS 13. - Verify persistence by toggling a local
@Statethat forces a body recomputation and checking that the view model’s data is unchanged.
With @StateObject you can write more predictable SwiftUI code, reduce boilerplate, and ensure that your view models survive view reloads without leaking resources.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.