State and data flow · core
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?
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.