← All lessonsGuide · Binding, @Binding and @Bindable
Guide

Binding, @Binding and @Bindable

From 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.

1 · from lesson 17

Start here: make a Binding yourself

A 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 $:

A Binding, made by hand
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

Console
the binding reads 1
after writing through the binding, stored is 2
after changing stored, the binding reads 3

The 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.

2 · from lesson 17

What it is made of

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:

MyBinding: two closures and a wrappedValue
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:

The same program, with MyBinding
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

Console
the binding reads 1
after writing through the binding, stored is 2
after changing stored, the binding reads 3

One 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.

3 · from lesson 17

Give one to a view

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:

A child that asks for a Binding, and two parents that give it one
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.

4 · from lesson 17

@Binding: the same thing, with nicer syntax

The 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:

MyBinding, now a property wrapper: @propertyWrapper and projectedValue are new
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:

A child using the wrapper
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:

The same child, written out
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:

Using both, then looking inside the real ones
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

Console
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 thisWith @Binding var value: IntWith let value: Binding<Int>
Read itWith @Binding var value: IntvalueWith let value: Binding<Int>value.wrappedValue
Write itWith @Binding var value: Intvalue += 1With let value: Binding<Int>value.wrappedValue += 1
Pass it downWith @Binding var value: Int$valueWith let value: Binding<Int>value
What the parent writesWith @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.

5 · from lesson 17

$value.property: a Binding to part of a value

When 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:

MyBinding, now with @dynamicMemberLookup and a subscript
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)
            }
        )
    }
}
A Binding to one property of a struct
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

Console
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 0

That 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.

6 · from lesson 18

A Binding to a property of an object

Now suppose the value lives in an object:

An observable 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:

A Binding to an object's property, by hand
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

Console
after writing through the binding, observableObject.property is 7

This 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.

7 · from lesson 18

@Bindable: that, for any property

So 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:

MyBindable: an object, and a subscript that makes a Binding
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
            }
        )
    }
}
Using it, then the real one
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

Console
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: Model

Look 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:

An object handed in with let
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:

From the iOS 27 SDK's Swift interface (shortened) — not code to type
@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:

@Bindable on a struct
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
8 · from lesson 18

What the real ones add

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:

From the iOS 27 SDK's Swift interface (shortened) — not code to type
@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:

Looking inside the real Binding
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 += 1

It prints

Console
  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.otherProperty

Making 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:

One parent and two children, in a real window
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()
    }
}
The run
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

Console
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 ran

The 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.

9 · from lesson 18

Which to use

It comes down to what the view has, and what it needs:

The viewWriteThen
Only reads an objectWritelet observableObject: ModelThenNo $ and nothing else needed
Is handed an object, and a control needs a BindingWrite@Bindable var observableObject: ModelThen$observableObject.property
Makes and owns the objectWrite@State private var observableObject = Model()Then$observableObject.property, no @Bindable needed
Edits one value, and needn't know where it livesWrite@Binding var value: IntThenvalue, and $value to pass it on
Needs a Binding that converts or checks the valueWriteBinding(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.

Further reading