State management is the core of SwiftUI.
It decides:
- how your UI updates
- how your logic flows
- how modules communicate
- how async tasks reflect in the UI
- how stable and predictable your app feels
But most apps mix state incorrectly β leading to:
- random UI glitches
- infinite re-renders
- ViewModel recreation
- broken bindings
- stale values
- async cancellation issues
This is the complete guide to mastering state management in SwiftUI β covering both classic tools and the modern @Observable model.
π₯ 1. The Four Primary State Tools in SwiftUI
SwiftUI gives you four main property wrappers:
| Wrapper | Lifetime | Usage |
|---|---|---|
| @State | Local to the view | Simple UI values |
| @Binding | Mirrors parent state | Passing state downward |
| @StateObject | Persistent across view reloads | ViewModels |
| @ObservedObject | Not persistent | For βtemporaryβ hosting of models |
Plus two global tools:
- @Environment
- @observable (new in 2023+, future-default in 2026)
Letβs break down when to use each.
π¦ 2. @State β For Local, Ephemeral UI Values
Use when the value:
- belongs only to this view
- resets when the view disappears
- is simple (Bool, Int, small struct)
Example:
struct LoginView: View {
@State private var email = ""
@State private var password = ""
@State private var loading = false
}
Never store heavy objects here.
π§ 3. @Binding β Passing State Downward
Use when:
- child views need to modify parent state
- two-way interaction is required
Slider(value: $volume)
Bindings should never be stored long-term in ViewModels.
π© 4. @StateObject β The Correct Place for ViewModels
Only use @StateObject for long-lived objects that:
- manage business logic
- handle async work
- must not be recreated on UI refresh
- own derived or computed state
Example:
@StateObject var viewModel = FeedViewModel()
This preserves identity across:
- animations
- view reloads
- tab switches
- parent state updates
Rule:
If recreating the object breaks your feature β it should be a @StateObject.
π₯ 5. @ObservedObject β Use Sparingly
@ObservedObject should be used when:
- the view does not own the object
- the parent provides it
- it's fine for the object to be recreated
It is not for ViewModels, except in rare hosted cases.
πͺ 6. @Environment β Global Read-Only State
Use @Environment for:
- system values
- externally provided configuration
- app-level services (if registered properly)
- theme / device metrics
- error presenters
- routing containers
Example:
@Environment(ErrorPresenter.self) var errors
Do NOT store large mutable objects here unless you're intentionally building an AppState container.
π§ 7. @observable β The Future of SwiftUI State
@observable replaces ObservableObject + @Published.
Example:
@Observable
class FeedViewModel {
var posts: [Post] = []
var loading = false
}
No @Published.
No objectWillChange.
No boilerplate.
It dramatically reduces:
- invalidations
- unpredictable updates
- object identity bugs
Recommendation:
Use @observable for all new ViewModels unless you need KVO or compatibility with older code.
π 8. Understanding SwiftUIβs Render Cycle (What Actually Triggers Updates)
A view re-renders when:
β a @State value changes
β an @observable property changes
β a @StateObject publishes a change
β a Binding value changes
β a parentβs body changes
A view does not re-render when:
β unrelated @State properties change
β async tasks finish but state is unchanged
β external services update without binding
Understanding this prevents 90% of performance bugs.
π§± 9. State Ownership Rules (The Architecture Part)
Use this simple checklist:
β UI owns UI state
(@State)
β View owns its ViewModel
(@StateObject)
β ViewModel owns business logic
(@observable)
β Services own side effects
(networking, storage, sync)
β AppState owns global shared data
(user session, cache, settings)
Incorrect:
Views owning services.
ViewModels containing views.
Models storing closures.
Bindings leaking upward.
π§© 10. Derived & Computed State (Avoid Heavy Work)
A common anti-pattern:
var filtered = posts.filter { ... } // computed every refresh
Instead:
- compute inside the ViewModel
- memoize expensive transforms
- store the derived result
@Observable
class FeedViewModel {
var posts: [Post] = []
var search = ""
var filtered: [Post] {
posts.filter { $0.title.contains(search) }
}
}
SwiftUI only recomputes when dependent data changes.
π§΅ 11. Handling Async State Safely
Async tasks must bind state to the main actor:
@MainActor
func load() async {
loading = true
defer { loading = false }
do {
posts = try await api.fetch()
} catch {
errors.present(map(error))
}
}
This prevents:
- UI updates from background threads
- race conditions
- stale state
- invalid mutations
π‘ 12. Avoiding the Biggest State Bug: Identity Loss
This happens when SwiftUI recreates a ViewModel accidentally:
FeedView(viewModel: FeedViewModel()) // β recreated every render
Solutions:
@StateObject var viewModel = FeedViewModel() // Best for view-owned VM
FeedView(viewModel: existingVM) // Best for parent-controlled
or for navigation:
.navigationDestination(item: $selected) { item in
DetailView(itemID: item.id)
}
π§ 13. Global State (AppState Containers)
For shared app-wide state:
@Observable
class AppState {
var session: UserSession?
var tabSelection = 0
var pendingDeepLink: AppRoute?
}
Injected into environment:
.environment(appState)
This enables:
- shared navigation
- session flows
- offline behavior
- deep linking
But be careful β global state is powerful and should be minimized.
β‘ 14. Performance Rules
β Keep @State small
β Prefer @observable over @Published
β Push expensive work out of views
β Memoize derived state
β Keep ViewModels reference types
β Do not store large models in @State
β Use .task instead of .onAppear for async work
These patterns keep your UI responsive.
π Final Thoughts
State management is the foundation of great SwiftUI architecture.
Mastering it unlocks:
- predictable UI
- smooth async flows
- stable navigation
- reusable modules
- scalable architecture
- simpler ViewModels
This guide gives you the full modern toolkit used in production SwiftUI apps.
Top comments (0)