SwiftUI State Management
Irbisa · cheatsheetSeptember 13, 2026

SwiftUI State Management

@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, Observation

Middle Developer20 itemscompressed for a skim
  1. 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") }
    …
  2. 02

    @StateObject vs @ObservedObject: what breaks if you pick the wrong one?

    Medium

    Both 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) }
    }
    …
  3. 03

    How do you pass shared state down a SwiftUI tree without prop-drilling?

    Medium

    SwiftUI'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 }
    }
    …
  4. 04

    What does the @Observable macro change compared with ObservableObject and @Published?

    Medium

    The 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] = []
    …
  5. 05

    What makes SwiftUI re-run a view's body, and when does its @State get thrown away?

    Hard

    SwiftUI re-runs body when 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() }
    …
  6. 06

    Which SwiftUI modifiers run async work in a view, and what happens to that work when the view disappears?

    Medium

    Two 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
    …
  7. 07

    Where do side effects belong in SwiftUI, if body has to stay pure?

    Medium

    body must 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
    …
  8. 08

    How do you hand a child view a Binding when the value is not stored in @State?

    Medium

    A 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 {
    …
  9. 09

    The environment pushes values down the view tree — how do you send a value back up, such as a child's measured height?

    Hard

    Preferences are SwiftUI's upward channel: a child writes a value with preference(key:value:) and an ancestor reads the combined result with onPreferenceChange.

    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. 10

    What do @AppStorage and @SceneStorage do, and where do they stop being the right tool?

    Medium

    @AppStorage is a view-observable window onto UserDefaults, and @SceneStorage is 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. 11

    Which built-in environment values earn their keep, and how does a child view dismiss the screen it is on?

    Easy

    A child dismisses itself by reading the dismiss action 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. 12

    A SwiftUI screen stutters and you suspect too many body evaluations — how do you confirm it and what do you change?

    Hard

    Prove 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. 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

    @FocusState is 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. 14

    You keep a plain class in @State, change one of its properties, and the screen never redraws — why does nothing happen?

    Easy

    @State invalidates 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. 15

    How does state flow in and out of a UIViewRepresentable, and why does the wrapped view sometimes fight the user's typing?

    Medium

    State flows down through the representable's stored properties in updateUIView and 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. 16

    Edits made on a detail screen never appear in the list behind it, even though both draw the same order — what went wrong?

    Medium

    The detail screen was handed a copy: Order is a struct, so let order: Order gives 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. 17

    A row's expanded/collapsed toggle keeps resetting after the user scrolls it off screen and back — what is actually happening?

    Medium

    That flag lives in the row's @State, which is bound to the row view's lifetime, and a List or LazyVStack ends 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. 18

    A screen keeps isLoading, errorMessage and items as three separate properties and sometimes shows a spinner over an error — how do you fix the shape?

    Medium

    Three 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. 19

    How does @Observable know which views to invalidate, and what kinds of change does that tracking silently miss?

    Hard

    The macro rewrites every stored property into a computed one that reports reads and writes to an ObservationRegistrar, and SwiftUI evaluates body inside withObservationTracking, 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. 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?

    Hard

    Every write a view can observe has to happen on the main actor: SwiftUI's views, @State and 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() {
    …