Binding, @Binding and @BindableFrom one Binding you make by hand, up to the two property wrappers that write it for you, with a small implementation of each that compiles and runs
You have been writing $ for a while now, and it can look as if three different things are going on: Binding, @Binding and @Bindable. There is really one thing, a Binding, and two conveniences for getting one. This guide starts with a Binding you make by hand, with no wrapper and no $, and adds one piece at a time. Each piece comes with a small implementation that compiles and runs, and what it prints is on the page.
Binding yourself · lesson 17@Binding: the same thing, with nicer syntax · lesson 17$value.property: a Binding to part of a value · lesson 17Binding to a property of an object · lesson 18@Bindable: that, for any property · lesson 18Binding yourselfA Binding does two jobs: it reads a value that lives somewhere else, and it writes that value back. You can make one from two closures, one for each job. There is no view here, no property wrapper and no $:
var stored = 1
let binding = Binding(
get: {
stored
},
set: { newValue in
stored = newValue
}
)
print("the binding reads", binding.wrappedValue)
binding.wrappedValue = 2
print("after writing through the binding, stored is", stored)
stored = 3
print("after changing stored, the binding reads", binding.wrappedValue)It prints
the binding reads 1
after writing through the binding, stored is 2
after changing stored, the binding reads 3The value lives in stored, and the Binding is a way to reach it. Writing through the Binding changes stored, and changing stored shows up when the Binding is read. wrappedValue is the property that does the reading and writing.
You could write that type yourself. It is a struct that holds the two closures, and a wrappedValue that calls them. It is a small model of the real one, called MyBinding so that the two can't be confused:
import SwiftUI
struct MyBinding<Value> {
let getValue: () -> Value
let setValue: (Value) -> Void
var wrappedValue: Value {
get {
getValue()
}
nonmutating set {
setValue(newValue)
}
}
}Used the same way, it behaves the same way:
var stored = 1
let binding = MyBinding(
getValue: {
stored
},
setValue: { newValue in
stored = newValue
}
)
print("the binding reads", binding.wrappedValue)
binding.wrappedValue = 2
print("after writing through the binding, stored is", stored)
stored = 3
print("after changing stored, the binding reads", binding.wrappedValue)It prints
the binding reads 1
after writing through the binding, stored is 2
after changing stored, the binding reads 3One word is doing real work. nonmutating set says that writing to wrappedValue does not change the struct, and it doesn't: the struct still holds the same two closures, and the value that changes is somewhere else. That is why a let binding can be written to. It is also why a copy of a Binding reaches the same value.
This is what a Binding is for. A Stepper needs to read a number and write it back, and it should not need to know where the number is kept. So it asks for a Binding. A view of your own can ask for one the same way, as a plain property:
import SwiftUI
struct Child: View {
let value: Binding<Int>
var body: some View {
Stepper("\(value.wrappedValue)", value: value)
}
}
struct ParentByHand: View {
@State private var stored = 0
var body: some View {
Child(value: Binding(
get: {
stored
},
set: { newValue in
stored = newValue
}
))
}
}
struct ParentWithDollar: View {
@State private var stored = 0
var body: some View {
Child(value: $stored)
}
}The child is the same for both parents. The first builds its Binding by hand. In the second, $stored is @State handing over a Binding it makes for you, of the same type. So far there is no @Binding anywhere: let value: Binding<Int> is enough.
@Binding: the same thing, with nicer syntaxThe only inconvenience in that child is value.wrappedValue. You want to say value. A property wrapper is a type the compiler will write that for: mark MyBinding with @propertyWrapper, and it needs a wrappedValue, which it already has. Add a projectedValue, which is what $ gives you, and here it is simply the Binding itself:
import SwiftUI
@propertyWrapper
struct MyBinding<Value> {
let getValue: () -> Value
let setValue: (Value) -> Void
var wrappedValue: Value {
get {
getValue()
}
nonmutating set {
setValue(newValue)
}
}
var projectedValue: MyBinding<Value> {
self
}
}Now a child can declare @MyBinding var value: Int, and inside it value is an Int:
import SwiftUI
struct Child {
@MyBinding var value: Int
func addOne() {
value += 1
}
func passItDown() -> MyBinding<Int> {
$value
}
}There is nothing more to it than the compiler writing this for you. Here is the same child with that written out by hand:
import SwiftUI
struct ChildWrittenOut {
private let _value: MyBinding<Int>
init(value: MyBinding<Int>) {
_value = value
}
var value: Int {
get {
_value.wrappedValue
}
nonmutating set {
_value.wrappedValue = newValue
}
}
// What $value means. Swift keeps names that start with $ for itself, so this one has another name.
var dollarValue: MyBinding<Int> {
_value.projectedValue
}
func addOne() {
value += 1
}
func passItDown() -> MyBinding<Int> {
dollarValue
}
}Both are fed the same way, and the real @Binding stores exactly what a plain let stores, under a different name:
var stored = 1
let binding = MyBinding(
getValue: {
stored
},
setValue: { newValue in
stored = newValue
}
)
let child = Child(value: binding)
child.addOne()
print("after child.addOne(), stored is", stored)
let writtenOut = ChildWrittenOut(value: binding)
writtenOut.addOne()
print("after writtenOut.addOne(), stored is", stored)
child.passItDown().wrappedValue = 10
print("after writing through child's $value, stored is", stored)
print("the real wrapper stores:", realFields(RealChild(value: .constant(0))))
print("let stores:", realFields(RealLetChild(value: .constant(0))))It prints
after child.addOne(), stored is 2
after writtenOut.addOne(), stored is 3
after writing through child's $value, stored is 10
the real wrapper stores: _value: Binding<Int>
let stores: value: Binding<Int>| To do this | With @Binding var value: Int | With let value: Binding<Int> |
|---|---|---|
| Read it | With @Binding var value: Intvalue | With let value: Binding<Int>value.wrappedValue |
| Write it | With @Binding var value: Intvalue += 1 | With let value: Binding<Int>value.wrappedValue += 1 |
| Pass it down | With @Binding var value: Int$value | With let value: Binding<Int>value |
| What the parent writes | With @Binding var value: IntChild(value: $stored) | With let value: Binding<Int>Child(value: $stored) |
So @Binding makes nothing new. It is a stored Binding, and syntax that lets the view treat it as the value.
$value.property: a Binding to part of a valueWhen the value is a struct, you often want a Binding to one of its properties. That takes one more feature, @dynamicMemberLookup, which turns whole.property into a call to a subscript with a key path. The new Binding reads the whole value and picks out the property. To write, it reads the whole value, changes the property, and writes the whole value back:
import SwiftUI
@dynamicMemberLookup
@propertyWrapper
struct MyBinding<Value> {
let getValue: () -> Value
let setValue: (Value) -> Void
var wrappedValue: Value {
get {
getValue()
}
nonmutating set {
setValue(newValue)
}
}
var projectedValue: MyBinding<Value> {
self
}
subscript<Property>(
dynamicMember keyPath: WritableKeyPath<Value, Property>
) -> MyBinding<Property> {
MyBinding<Property>(
getValue: {
getValue()[keyPath: keyPath]
},
setValue: { newValue in
var whole = getValue()
whole[keyPath: keyPath] = newValue
setValue(whole)
}
)
}
}struct Pair {
var property = 0
var otherProperty = 0
}
var stored = Pair()
let whole = MyBinding(
getValue: {
stored
},
setValue: { newValue in
print("the whole Pair is written back:", newValue)
stored = newValue
}
)
let part = whole.property
print("whole.property is a", type(of: part))
part.wrappedValue = 5
print("stored.property is", stored.property, "and stored.otherProperty is", stored.otherProperty)It prints
whole.property is a MyBinding<Int>
the whole Pair is written back: Pair(property: 5, otherProperty: 0)
stored.property is 5 and stored.otherProperty is 0That is the whole of MyBinding, and it is the shape of the real Binding: a wrapped value, a projected value, and a subscript that gives you a Binding to a property.
Binding to a property of an objectNow suppose the value lives in an object:
import SwiftUI
@Observable
final class Model {
var property = 0
var otherProperty = 0
}You already know how to get a Binding to its property, because it is the first thing on this page. The closures just reach into the object:
let observableObject = Model()
let binding = Binding(
get: {
observableObject.property
},
set: { newValue in
observableObject.property = newValue
}
)
binding.wrappedValue = 7
print("after writing through the binding, observableObject.property is", observableObject.property)It prints
after writing through the binding, observableObject.property is 7This works, and it is all a Binding to an object's property ever is. But you would write those closures again for every property of every object. The only things that change are the object and the property, and a key path can name the property.
@Bindable: that, for any propertySo hold the object, and make the Binding when asked for a property. That is all of @Bindable, and this small version of it is built from pieces you have already seen:
import SwiftUI
@dynamicMemberLookup
@propertyWrapper
struct MyBindable<Value: AnyObject & Observable> {
var wrappedValue: Value
var projectedValue: MyBindable<Value> {
self
}
subscript<Property>(
dynamicMember keyPath: ReferenceWritableKeyPath<Value, Property>
) -> Binding<Property> {
let object = wrappedValue
return Binding(
get: {
object[keyPath: keyPath]
},
set: { newValue in
object[keyPath: keyPath] = newValue
}
)
}
}let model = Model()
@MyBindable var observableObject = model
let property: Binding<Int> = $observableObject.property
let otherProperty: Binding<Int> = $observableObject.otherProperty
property.wrappedValue = 5
otherProperty.wrappedValue = 6
print("model.property is", model.property, "and model.otherProperty is", model.otherProperty)
print("MyBindable stores:", realFields($observableObject))
withObservationTracking {
_ = model.property
} onChange: {
print("told: someone who read model.property")
}
property.wrappedValue = 100
@Bindable var real = model
print("$real is a", type(of: $real), "and $real.property is a", type(of: $real.property))
print("the real Bindable stores:", realFields($real))It prints
model.property is 5 and model.otherProperty is 6
MyBindable stores: wrappedValue: Model
told: someone who read model.property
$real is a Bindable<Model> and $real.property is a Binding<Int>
the real Bindable stores: wrappedValue: ModelLook at what $ gives you here. For @Binding, the projected value was a Binding. For @Bindable it is the wrapper itself, because there is no single value to bind to: there is an object, and any of its properties might be asked for. $observableObject.property then goes through the subscript and comes back as an ordinary Binding<Int>.
So @Bindable is an adapter. It stores only the object, and nothing in it observes anything. The closures use the property's normal getter and setter, and those do the observing, which is why the write above told the reader of model.property (the @Observable guide follows that path). It does not own the object either: something else, such as @State in a parent, keeps it alive.
Without a wrapper there is no $, and the compiler's second message follows from its first:
import SwiftUI
@Observable
final class Model {
var property = 0
}
struct NoWrapper: View {
let observableObject: Model
var body: some View {
Stepper("Property", value: $observableObject.property)
}
}The compiler says
cannot find '$observableObject' in scope
generic parameter 'V' could not be inferred
note: in call to initializer
The real Bindable only accepts an observable class, and the way it does that is worth seeing. The type itself takes any Value. Its general init is marked unavailable, with a message, and the init you actually use exists only in an extension for classes that conform to Observable:
@dynamicMemberLookup @propertyWrapper public struct Bindable<Value> {
public var wrappedValue: Value
public var projectedValue: Bindable<Value> { get }
@available(*, unavailable,
message: "The wrapped value must be an object that conforms to Observable")
public init(wrappedValue: Value)
}
extension Bindable where Value: AnyObject {
public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<Value, Subject>
) -> Binding<Subject> { get }
}
extension Bindable where Value: AnyObject, Value: Observable {
public init(wrappedValue: Value)
public init(_ wrappedValue: Value)
}For an observable class both inits fit, and Swift picks the more specific one. For anything else only the unavailable one fits, so choosing it is an error, and the error is Apple's message:
import SwiftUI
struct NotAClass {
var property = 0
}
struct OnAStruct: View {
@Bindable var value: NotAClass
var body: some View {
Text("\(value.property)")
}
}The compiler says
'init(wrappedValue:)' is unavailable: The wrapped value must be an object that conforms to Observable
note: 'init(wrappedValue:)' has been explicitly marked unavailable here
The models above behave like the real types for everything so far. The real Binding is different inside, though, and two of those differences show up in your app. Its source is not public, but the SDK's interface shows its stored properties:
@frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
public var transaction: Transaction
package var location: AnyLocation<Value>
package var _value: Value
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
public init(projectedValue: Binding<Value>)
public subscript<Subject>(
dynamicMember keyPath: WritableKeyPath<Value, Subject>
) -> Binding<Subject> { get }
}It does not hold two closures. It holds a location, an object that knows how to read and write the value, and it also keeps a copy of the value in _value. Here is what $observableObject.property really contains:
let model = Model()
@Bindable var observableObject = model
let binding = $observableObject.property
for field in Mirror(reflecting: binding).children {
print(" \(field.label ?? "?"): \(type(of: field.value))")
}
print("made when property was 0, then property is set to 8:")
model.property = 8
print(" stored copy:", Mirror(reflecting: binding).children.first { $0.label == "_value" }!.value)
print(" wrappedValue:", binding.wrappedValue)
withObservationTracking {
_ = $observableObject.otherProperty
} onChange: {
print("told, after only making $observableObject.otherProperty")
}
model.otherProperty += 1It prints
transaction: Transaction
location: LocationBox<ObjectLocation<Model, Int>>
_value: Int
made when property was 0, then property is set to 8:
stored copy: 0
wrappedValue: 8
told, after only making $observableObject.otherPropertyMaking a Binding reads the property. The Binding takes its copy of the value when it is made, and that read goes through the getter, so it counts as a read. A body that only passes $observableObject.property to a child, and never shows it, is still signed up for that property.
A location can be compared, and closures can't. The location holds the object and the key path, so two Binding values to the same property can be recognized as the same. A closure written in a body is a new closure each time the body runs. In this window, a parent re-runs four times for a reason that has nothing to do with the object, and it feeds two children, one each way:
import AppKit
import SwiftUI
@Observable
final class Model {
var property = 0
}
final class Driver {
var rerunParent: () -> Void = {
}
}
struct Child: View {
let name: String
@Binding var value: Int
var body: some View {
let _ = print(" child fed with \(name): body ran")
Stepper("\(value)", value: $value)
}
}
struct Parent: View {
@Bindable var observableObject: Model
let driver: Driver
@State private var reruns = 0
var body: some View {
let _ = print(" parent: body ran, reruns is \(reruns)")
VStack {
Child(name: "$observableObject.property", value: $observableObject.property)
Child(name: "a hand-made Binding", value: Binding(
get: {
observableObject.property
},
set: { newValue in
observableObject.property = newValue
}
))
}
.onAppear {
driver.rerunParent = {
reruns += 1
}
}
}
}
func settle(_ view: NSView) {
for _ in 0..<5 {
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))
view.layoutSubtreeIfNeeded()
view.display()
}
}let app = NSApplication.shared
app.setActivationPolicy(.prohibited)
let driver = Driver()
print("the first draw:")
let host = NSHostingView(rootView: Parent(observableObject: Model(), driver: driver))
let window = NSWindow(
contentRect: NSRect(x: -5000, y: -5000, width: 300, height: 100),
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.contentView = host
settle(host)
for tap in 1...4 {
print("the parent re-runs, time \(tap), and nothing about the object changed:")
driver.rerunParent()
settle(host)
}It prints
the first draw:
parent: body ran, reruns is 0
child fed with $observableObject.property: body ran
child fed with a hand-made Binding: body ran
the parent re-runs, time 1, and nothing about the object changed:
parent: body ran, reruns is 1
child fed with a hand-made Binding: body ran
child fed with $observableObject.property: body ran
the parent re-runs, time 2, and nothing about the object changed:
parent: body ran, reruns is 2
child fed with a hand-made Binding: body ran
the parent re-runs, time 3, and nothing about the object changed:
parent: body ran, reruns is 3
child fed with a hand-made Binding: body ran
the parent re-runs, time 4, and nothing about the object changed:
parent: body ran, reruns is 4
child fed with a hand-made Binding: body ranThe child fed by hand runs its body every time, because its Binding looks new each time. It still works correctly. It just does more work than it needs to, and that is the reason to prefer $ where it will do. The child fed with $ runs once more, on the first re-run, and then stops; why it takes that one extra run is not known. How SwiftUI makes this decision is not public, so read this as what happens rather than how: the comparison explains the result, but it has not been traced inside SwiftUI.
If you want to read a complete version, OpenSwiftUI's Bindable and its ObjectLocation are open source. They are a third party's recreation and not Apple's code, but the names LocationBox and ObjectLocation are the ones the real Binding printed above.
It comes down to what the view has, and what it needs:
| The view | Write | Then |
|---|---|---|
| Only reads an object | Writelet observableObject: Model | ThenNo $ and nothing else needed |
Is handed an object, and a control needs a Binding | Write@Bindable var observableObject: Model | Then$observableObject.property |
| Makes and owns the object | Write@State private var observableObject = Model() | Then$observableObject.property, no @Bindable needed |
| Edits one value, and needn't know where it lives | Write@Binding var value: Int | Thenvalue, and $value to pass it on |
Needs a Binding that converts or checks the value | WriteBinding(get:set:) | ThenWritten by hand, because the wrappers only hand over the property as it is |
The last row is the reason to know the first section. $ can only give you the property as it is, so a Slider, which wants a floating-point value, can't use a Binding<Int>. Two closures can convert it in both directions.