SwiftUI State Management
@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, Observation
- 01
Explain @State and @Binding in SwiftUI.
Easy@State — a property wrapper that gives a SwiftUI view local mutable storage.
struct Counter: View { @State private var value = 0 var body: some View { VStack { Text("\(value)").font(.largeTitle) Stepper(value: $value, in: 0...100) { Text("Count") } … - 02
@StateObject vs @ObservedObject: what breaks if you pick the wrong one?
MediumBoth wrappers observe an
ObservableObject, and the difference is ownership: @StateObject keeps the object alive across body re-evaluations, @ObservedObject does not.// Legacy pair — ObservableObject + @Published final class CartViewModel: ObservableObject { @Published var items: [String] = [] func add(_ item: String) { items.append(item) } } … - 03
How do you pass shared state down a SwiftUI tree without prop-drilling?
MediumSwiftUI's environment is a dependency-injection channel scoped to a view subtree: you inject once near the root and any descendant reads it directly.
@Observable final class AppSession { var currentUser: User? func login(_ user: User) { currentUser = user } func logout() { currentUser = nil } } … - 04
What does the @Observable macro change compared with ObservableObject and @Published?
MediumThe Observation framework makes every stored property of a class observable and narrows re-rendering to the properties a view actually read.
import SwiftUI import Observation @Observable final class FeedVM { var posts: [Post] = [] … - 05
What makes SwiftUI re-run a view's body, and when does its @State get thrown away?
HardSwiftUI re-runs
bodywhen a dependency that view read has changed, and it throws the view's state away when the view's identity changes.// Structural identity reset struct Demo: View { @State var showHeader = false var body: some View { VStack { if showHeader { HeaderView() } … - 06
Which SwiftUI modifiers run async work in a view, and what happens to that work when the view disappears?
MediumTwo modifiers tie a SwiftUI view to Swift Concurrency, and both scope the async work to the view's lifetime instead of to a Task you manage by hand.
@Observable final class FeedVM { var posts: [Post] = [] var error: Error? var isLoading = false func reload() async { isLoading = true … - 07
Where do side effects belong in SwiftUI, if
bodyhas to stay pure?Mediumbodymust be a pure function of state, so anything with a side effect goes in a lifecycle modifier —onChange,task,onAppear— and never in the body itself.struct SearchScreen: View { @State private var query = "" @State private var results: [Hit] = [] @State private var sortOrder: SortOrder = .newest @Environment(\.scenePhase) private var scenePhase … - 08
How do you hand a child view a Binding when the value is not stored in
@State?MediumA Binding is nothing but a getter plus a setter, so you can build one by hand with
Binding(get:set:)whenever the value lives somewhere the$prefix cannot reach.struct ProfileForm: View { @Bindable var user: User // @Observable class: $user.name projects a Binding @State private var nickname: String? var body: some View { Form { … - 09
The environment pushes values down the view tree — how do you send a value back up, such as a child's measured height?
HardPreferences are SwiftUI's upward channel: a child writes a value with
preference(key:value:)and an ancestor reads the combined result withonPreferenceChange.struct HeightKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) // how sibling contributions are merged } } … - 10
What do
@AppStorageand@SceneStoragedo, and where do they stop being the right tool?Medium@AppStorageis a view-observable window onto UserDefaults, and@SceneStorageis per-scene state restoration that the system saves and hands back when the app is relaunched.struct SettingsView: View { @AppStorage("showBadges") private var showBadges = true @AppStorage("theme") private var theme: Theme = .system // RawRepresentable<String> @SceneStorage("selectedTab") private var selectedTab = Tab.inbox // per window, restored var body: some View { … - 11
Which built-in environment values earn their keep, and how does a child view dismiss the screen it is on?
EasyA child dismisses itself by reading the
dismissaction out of the environment and calling it, because the environment already knows whether the view sits in a sheet or on a navigation stack.struct DetailScreen: View { @Environment(\.dismiss) private var dismiss @Environment(\.scenePhase) private var scenePhase @Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.openURL) private var openURL @Environment(\.isEnabled) private var isEnabled … - 12
A SwiftUI screen stutters and you suspect too many body evaluations — how do you confirm it and what do you change?
HardProve which views are re-rendering, and why, before you change any code.
struct FeedRow: View { let item: Item var body: some View { let _ = Self._printChanges() // debug only: prints what invalidated this body HStack { … - 13
The Done button on your form's keyboard does nothing and Next never jumps to the following field — which property wrapper is missing?
Easy@FocusStateis the only way to read or move keyboard focus from SwiftUI code: you bind it to controls with.focused(...), and writing to it focuses a field or dismisses the keyboard.struct SignUpForm: View { enum Field { case email, password } @State private var email = "" @State private var password = "" @FocusState private var focused: Field? … - 14
You keep a plain class in
@State, change one of its properties, and the screen never redraws — why does nothing happen?Easy@Stateinvalidates the view when the value it stores is replaced, and mutating a property of a class never replaces the reference it stores.// Nothing redraws: the reference stored in @State never changes final class Draft { var title = "" } struct BadEditor: View { … - 15
How does state flow in and out of a
UIViewRepresentable, and why does the wrapped view sometimes fight the user's typing?MediumState flows down through the representable's stored properties in
updateUIViewand back up through the coordinator, which is the only half of the pair that survives a re-render.struct SearchBar: UIViewRepresentable { @Binding var text: String var onCommit: () -> Void func makeCoordinator() -> Coordinator { Coordinator(self) } … - 16
Edits made on a detail screen never appear in the list behind it, even though both draw the same order — what went wrong?
MediumThe detail screen was handed a copy:
Orderis a struct, solet order: Ordergives the child its own value and every edit lands on that copy.struct Order: Identifiable { let id: UUID; var note: String; var quantity: Int } // The child edits its own copy — writes go nowhere struct BadDetail: View { @State private var order: Order // seeded once, never written back init(order: Order) { _order = State(initialValue: order) } … - 17
A row's expanded/collapsed toggle keeps resetting after the user scrolls it off screen and back — what is actually happening?
MediumThat flag lives in the row's
@State, which is bound to the row view's lifetime, and aListorLazyVStackends that lifetime when the row scrolls far enough out of view.// Row-local state: reset whenever the row is discarded off screen struct BadRow: View { let item: Item @State private var isExpanded = false var body: some View { … - 18
A screen keeps
isLoading,errorMessageanditemsas three separate properties and sometimes shows a spinner over an error — how do you fix the shape?MediumThree independent properties can express states that cannot happen, so collapse them into one enum the view switches over — the screen then has exactly as many states as the designer drew.
// Sixteen representable combinations, four of them real @Observable final class BadFeedModel { var isLoading = false var items: [Item] = [] var errorMessage: String? } … - 19
How does
@Observableknow which views to invalidate, and what kinds of change does that tracking silently miss?HardThe macro rewrites every stored property into a computed one that reports reads and writes to an
ObservationRegistrar, and SwiftUI evaluatesbodyinsidewithObservationTracking, so a view depends on exactly the properties it touched during that evaluation.// Roughly what @Observable expands to final class Model: Observable { @ObservationIgnored private var _title = "" @ObservationIgnored private let registrar = ObservationRegistrar() var title: String { … - 20
Your model writes its properties from a background network callback and the list updates late, glitches, or crashes — what does SwiftUI actually require here?
HardEvery write a view can observe has to happen on the main actor: SwiftUI's views,
@Stateand the observation machinery are all@MainActor-isolated, so a write from anywhere else is a data race, not merely a slow update.// Off-main writes: a runtime warning with ObservableObject, silent corruption with @Observable @Observable final class BadFeed { var items: [Item] = [] private let url = URL(string: "https://example.com/feed")! func load() { …