What is a Binding really, and how do derived, constant and transforming bindings behave?

8.10.2 · L2 · Interview

Predict first

What do these bindings do when written?

import SwiftUI

struct Profile { var name = "Ada"; var tags = ["x", "y"] }
nonisolated(unsafe) var model = Profile()
nonisolated(unsafe) var writes: [String] = []

let whole = Binding(get: { model }, set: { model = $0; writes.append("set") })
whole.name.wrappedValue = "Grace"
print("derived from a struct binding:", model.name, writes)

let constant = Binding.constant(7)
constant.wrappedValue = 99
print("constant ignores writes:", constant.wrappedValue)

whole.tags[0].wrappedValue = "first"
print("key path into an array:", model.tags)

let upper = Binding(get: { model.name.uppercased() }, set: { model.name = $0.lowercased() })
upper.wrappedValue = "HELLO"
print("transforming binding:", model.name, upper.wrappedValue)
  • derived binding writes through (one set) / constant stays 7 / array element changed / stored "hello", shown "HELLO"
  • constant becomes 99
  • derived write does not reach the model

Mental model. A binding is a remote control, not a TV: pressing its buttons changes the owner’s channel, and a constant binding is a remote with dead batteries.

A Binding stores no value: it is a getter and a setter (plus a transaction). $state hands you one that reads and writes the state’s storage; Binding(get:set:) builds one from any two closures, so the “source of truth” can be a model, a computed value or a database. The check shows the forms: writing through a binding derived from a struct binding goes through its setter (writes shows one set), .constant(7) accepts writes and silently ignores them (use it in previews and for read-only controls), a key path such as whole.tags[0] projects a binding to one element, and a transforming binding can store lowercase while presenting uppercase.

Because a binding is only closures, two things follow. Setters run on every write, so put validation and side effects there only if they are cheap, and remember a derived binding calls the parent’s setter with the whole modified value. And a binding does not keep the data alive or observe it: the view that owns the state still decides when to re-render. Converting an optional binding with Binding(optionalBinding) gives you a non-optional one for a sheet or a text field, but reading it after the underlying value became nil traps, so prefer an if let in the view or an item-based API (sheet(item:)) that removes the view when the value disappears. I did not run that crash here: outside a real view graph the conversion misbehaves, so the check does not cover it.

A binding is a getter and setter; constant ignores the setter.

Common traps

  • Putting heavy work in a Binding(set:) closure. It runs for every keystroke through a text field.
  • Forcing an optional binding to non-optional. It crashes if the value becomes nil while the child is still visible.

They might follow up with

  • How do you bind to a property of an @Observable object you were handed? @Bindable var model, then $model.name.
  • What does $ give for a @Binding? The binding itself.

Related questions

  • 8.3.1 What do @State, @Binding and @Bindable do, and who owns the data?
  • 8.10.1 A view’s init runs on every parent update. When does the initial value of @State or @StateObject actually get…
  • 8.10.3 What exactly does Observation track: computed properties, ignored properties, collections, nested objects and…

← A view’s init runs on every parent update. When does the initial…   What exactly does Observation track: computed properties, ignored… →