Binding
A read-write reference to state that lives somewhere else.
A Binding is a two-way connection to a value owned by another view. It doesn't store anything — it's a pair of closures, a getter and a setter, pointing at somebody else's storage.
That's what lets a child mutate a parent's state without owning it, and without the parent passing down a callback for every field. TextField, Toggle and Slider all take one, which is why they can write back the value you handed them.
You make one with the $ prefix on a @State property. Data flows down as a value, changes flow back up through the setter.
@Binding declares the dependency; $ creates one to pass on.struct ParentView: View { @State private var isOn = false // owns the storage
var body: some View { VStack { SettingRow(title: "Notifications", isOn: $isOn) // $ projects a Binding Text(isOn ? "On" : "Off") // parent sees the change } }}
struct SettingRow: View { let title: String @Binding var isOn: Bool // no storage — a window onto the parent's
var body: some View { Toggle(title, isOn: $isOn) // $ again, to pass it further down }}
// A derived binding: transform on the way in and outextension Binding where Value == String { var trimmed: Binding<String> { Binding(get: { wrappedValue.trimmingCharacters(in: .whitespaces) }, set: { wrappedValue = $0 }) }}
// Previews and tests don't need real stateSettingRow(title: "Preview", isOn: .constant(true))
Show all 30 linesShow less
struct ParentView: View { @State private var isOn = false // owns the storage
var body: some View { VStack { SettingRow(title: "Notifications", isOn: $isOn) // $ projects a Binding Text(isOn ? "On" : "Off") // parent sees the change } }}
struct SettingRow: View { let title: String @Binding var isOn: Bool // no storage — a window onto the parent's
var body: some View { Toggle(title, isOn: $isOn) // $ again, to pass it further down }}
// A derived binding: transform on the way in and outextension Binding where Value == String { var trimmed: Binding<String> { Binding(get: { wrappedValue.trimmingCharacters(in: .whitespaces) }, set: { wrappedValue = $0 }) }}
// Previews and tests don't need real stateSettingRow(title: "Preview", isOn: .constant(true))Under the hood
Binding is a struct holding a get closure and a set closure. There's no storage in it at all, which is why passing one around is cheap and why a binding to a deallocated source is a real hazard rather than an impossible state.
The $ prefix is the property wrapper's *projected value*: @State exposes projectedValue of type Binding<Value>, and $count is sugar for _count.projectedValue. Any property wrapper can project something; @State, @Binding, @Bindable and @FocusState all project bindings.
Because it's just closures, you can build one by hand with Binding(get:set:) — useful for deriving a transformed binding, bridging to a non-SwiftUI model, or adapting an optional. .constant(_) is the same thing with a setter that discards, which is what previews want.
Quick check
What does the $ prefix actually give you?
- A copy of the value
- The property wrapper's projected value — for @State, a Binding to its storage
- A reference to the view
- An optional version of the value
Reveal answer
The property wrapper's projected value — for @State, a Binding to its storage
$count is sugar for _count.projectedValue. For @State and @Binding that projection is a Binding, which is a getter/setter pair pointing at the underlying storage. Other wrappers project other things — the $ is a general property-wrapper feature, not binding-specific syntax.
Interview question
When should a child view take a Binding rather than a plain value?
Think first, then reveal
When the child needs to write. A plain value is a one-way snapshot — the child can display it and nothing more. A Binding is a getter and setter pointing back at the owner's storage, so writes propagate.
The way I decide is by asking who owns the state. It should live with the view that's responsible for it — usually the highest view that cares — as @State. Anything below that either reads it, in which case a let is better because it documents that the child can't change anything, or writes it, in which case @Binding and pass $.
Getting this wrong produces a very characteristic bug: a toggle or text field that appears to work, animates correctly, and has no effect. There's no error, because passing a value where you meant a binding compiles fine — the child just ends up mutating its own copy.
The other reason to prefer let where you can is that it keeps the data flow readable. If every child takes a binding, you can't tell from a signature which ones actually mutate.
What an interviewer listens for
- Ties the choice to whether the child writes
- Places ownership at the highest view that cares
- Prefers let for read-only children, and says why
- Names the silent no-effect bug that results from getting it wrong
Key takeaway
A binding is a getter/setter pair pointing at someone else's storage. Pass $ when the child must write, and a plain value when it only reads.