State and bindings
How a struct that gets thrown away constantly can nevertheless remember things — and how a child view changes its parent's data.
By the end you will be able to
- Explain where @State actually stores its value and why views can be structs
- Pass write access to a child with @Binding and the $ prefix
- Choose between @State, @Binding and a plain let
A SwiftUI view is a struct that is destroyed and recreated constantly. So where does @State private var count = 0 keep its value between recreations?
@State — this view owns this value
struct Counter: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Add one") {
count += 1
}
}
}
}
Three things happen because of that one attribute:
- The value lives in SwiftUI's storage, not in the struct, so it survives re-creation.
- Writing to it marks the view as needing an update, and
bodyruns again. - You can mutate it from inside
body's closures even thoughselfis aletstruct.
@Binding — a child writes to the parent's value
A child view that only reads takes a plain let. A child that must write takes a @Binding:
struct Stepper2: View {
@Binding var value: Int // read-write reference to someone else's state
var body: some View {
HStack {
Button("−") { value -= 1 }
Text("\(value)")
Button("+") { value += 1 }
}
}
}
struct Parent: View {
@State private var quantity = 1
var body: some View {
Stepper2(value: $quantity) // $ makes a Binding from the State
}
}
The $ prefix gives you the property wrapper's projected value. For @State, that is a Binding — a pair of get and set closures pointing back at the original storage. The child does not own the value; it holds a two-way connection to it.
One QuantityRow type serves both rows, each bound to a different piece of the parent's state. That is the whole pattern.
Bindings everywhere
Every SwiftUI control that edits a value takes a binding:
TextField("Name", text: $name)
Toggle("Notifications", isOn: $enabled)
Slider(value: $volume, in: 0...1)
Picker("Theme", selection: $theme) { … }
You can also derive a binding from a property of a bound value — $user.name works if $user is a Binding<User> — which is why SwiftUI forms can edit nested model structs directly.
The decision table
| Situation | Use |
|---|---|
| This view owns the value and no one else touches it | @State private var |
| A child needs to change a value the parent owns | @Binding var, passed as $value |
| A child only needs to display it | plain let |
| Several unrelated views share it | an observable model — next lesson |
Reach for the least powerful option that works. A plain let is easier to reason about than a binding, and a binding is easier than shared model state.
The hidden storage property
Writing @State private var text = "" generates two things: a hidden stored property _text of type State<String>, and a computed text that reads and writes through it. $text is _text.projectedValue.
That is why you occasionally see this in real code:
init(initial: String) {
_text = State(initialValue: initial)
}
It seeds the state from an initializer. And it comes with the warning that makes it worth knowing about: that seeding happens only on first creation. If the parent later passes a different initial, this view ignores it, because SwiftUI keeps the existing storage for that position in the hierarchy.
Feeding changing external data into @State is one of the most common SwiftUI bugs. If the value comes from outside, it belongs in a let or a @Binding.
Two independent counters, each with its own @State. What is printed when this view is built?
struct Counter2: View {
let name: String
@State private var count = 0
var body: some View {
Text("\(name): \(count)")
}
}
func describe() {
print("Each Counter2 gets its own storage slot.")
print("Sharing a struct definition does not share state.")
}
describe()A child view needs to display a value but never change it. What should its property be?
Wire up a binding
ToggleRow should be able to change the parent's value. Fix its property and the way it is passed in.
Own the state
Give SearchBar its own private state for the query and show the current value beneath the field.
Explain why a SwiftUI view can be a let struct and still contain a counter that changes. Your explanation should mention where the value actually lives.
You can now:
- Explain where
@Statestores its value and why views can be structs - Pass write access downward with
@Bindingand$ - Apply the least-powerful-tool rule to
let,@Bindingand@State - Recognise the bug of feeding changing external data into
@State
Next up: @Observable — the modern way to share model state across many views.