SwiftUI State: Ownership Rules End View Redraw Bugs
Treat SwiftUI views as functions of state (UI = f(state)). Choose wrappers by ownership: @State for local simple values, @Binding to share edits, @StateObject for view-owned models, @ObservedObject for injected ones. Compute derived state, persist with @AppStorage.
Views as Functions of State: Redraws Are Features, Not Bugs
SwiftUI views are structs that act like pure functions: UI = f(state). When state changes, SwiftUI re-runs the body computation and redraws only affected parts—no manual reloadData() needed. State is any value the UI depends on, like a counter, toggle, or form validity.
Use @State for local, simple state owned by the view (Bool, Int, String, small structs). It persists across redraws because SwiftUI stores it stably:
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.title)
Button("Tap Me") {
count += 1
}
}
.padding()
}
}
Normal variables reset on redraws since views are disposable. For UI toggles:
struct ShowDetailsView: View {
@State private var showDetails = false
var body: some View {
VStack(spacing: 16) {
Button(showDetails ? "Hide Details" : "Show Details") {
showDetails.toggle()
}
if showDetails {
Text("Here are the details you definitely won't read.")
.padding()
.background(.thinMaterial)
.cornerRadius(12)
}
}
.padding()
}
}
Never store derived state—compute it to avoid sync bugs. Example form validation:
struct SignupView: View {
@State private var email = ""
@State private var password = ""
private var isValid: Bool {
email.contains("@") && password.count >= 8
}
// ...
Button("Create Account")
.disabled(!isValid)
}
Sharing and Model State: Bindings and Observable Objects
Pass ownership with @Binding: parent holds @State, child gets a binding like a remote control:
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "On" : "Off")
OnOffButton(isOn: $isOn)
}
}
}
struct OnOffButton: View {
@Binding var isOn: Bool
var body: some View {
Button(isOn ? "Turn Off" : "Turn On") {
isOn.toggle()
}
}
}
For complex state, use ObservableObject with @Published properties. View creating the object uses @StateObject to prevent recreation on redraws:
@MainActor
final class ProfileViewModel: ObservableObject {
@Published var name: String = "Sanjay"
@Published var status: String = "Trying not to over-engineer"
func updateStatus() {
status = "Still trying. Still failing."
}
}
struct ProfileView: View {
@StateObject private var vm = ProfileViewModel()
// ...
Button("Update Status") { vm.updateStatus() }
}
Injected objects use @ObservedObject. Avoid @ObservedObject var vm = ProfileViewModel()—it recreates and resets data/API calls.
For app-wide state (user login, theme), inject @EnvironmentObject at root:
struct RootView: View {
@StateObject private var appState = AppState()
var body: some View {
ContentView()
.environmentObject(appState)
}
}
// In any descendant:
@EnvironmentObject var appState: AppState
Limit to true globals; they hide dependencies.
Persistence and Decision Framework: Survive Restarts
Persist simple values across app kills with @AppStorage (UserDefaults-backed):
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
var body: some View {
Toggle("Dark Mode", isOn: $isDarkMode)
}
}
For scene/window-specific UI (drafts, tabs), use @SceneStorage:
struct NotesView: View {
@SceneStorage("draftText") private var draftText = ""
var body: some View {
TextEditor(text: $draftText)
}
}
Decision checklist by ownership/lifetime:
- Local/simple:
@State - Child edits:
@Binding - View-owned model:
@StateObject - Injected model:
@ObservedObject - App-shared:
@EnvironmentObject(sparingly) - App-persistent:
@AppStorage - Scene-persistent:
@SceneStorage
Pitfalls That Cause 90% of State Confusion
Common bugs: storing computed values (leads to staleness), wrong object wrapper (data resets), overusing globals (debug hell), fearing redraws (they're efficient). Answer: Who owns it? How long does it live? This shifts state from magic to predictable tools.