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)