What do @State, @Binding and @Bindable do, and who owns the data?

8.3.1 · L1 · Interview

Predict first

What does a custom binding read and write through?

import SwiftUI

final class Box: @unchecked Sendable { var x = 1 }
let box = Box()
let b = Binding(get: { box.x }, set: { box.x = $0 })
b.wrappedValue = 5
print(box.x, b.wrappedValue)
let c = Binding.constant(3)
c.wrappedValue = 9
print(c.wrappedValue)
  • 5 5, then 9
  • 5 5, then 3
  • 1 1, then 3

Mental model. @State is the notebook the view owns. A binding is a window onto someone else’s notebook through which you may read and write.

@State gives a view private, persistent storage. A view struct is recreated constantly, so SwiftUI keeps the actual value outside the struct, attached to the view’s identity in the graph. Changing it invalidates the view. Mark it private and use it for local UI state (a toggle, a text field’s contents); with a model class it holds the instance (@State private var model = Model()), which survives re-evaluation of the parent. @Binding is a two-way reference to state owned elsewhere: it stores no value, only a way to get and set one. You make it with the $ prefix ($name is the projected value of @State) and pass it to a child so the child can edit the parent’s data. Binding.constant(x) gives a read-only one for previews, and Binding(get:set:) builds a custom one.

@Bindable (with Observation) creates bindings to the properties of an @Observable object you were passed, so TextField("Name", text: $model.name) works without the model being @State. The rule of thumb is a single source of truth: exactly one owner (@State or a model), everyone else gets a binding or reads the value. Copying state into a second @State creates two truths that drift apart. Initialising @State from an init parameter sets it only once, when the view first appears, so later changes to the parameter do not update it. Prefer a plain property and a binding for data the parent owns.

A binding is just a getter and a setter onto storage owned elsewhere. A constant binding ignores writes.

Common traps

  • Copying a parent’s value into a child’s @State. It is captured once and never follows the parent.
  • Making @State non-private and setting it from outside. It blurs ownership; pass a binding instead.

They might follow up with

  • Where does @State storage actually live? In SwiftUI’s graph, keyed by the view’s identity, not in the struct.
  • What does $ give you? The property wrapper’s projected value, a Binding for @State.

Related questions

  • 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
  • 8.1.2 What is structural versus explicit identity in SwiftUI, and what happens to state when identity changes?
  • 8.3.2 How does the SwiftUI environment work, and when should you use it instead of passing a parameter?

How does the SwiftUI environment work, and when should you use it… →