Binding with @Observable Macro
Binding with @Observable
In SwiftUI, when using an @Observable
macro, the standard $
syntax (used to create bindings) doesn't work directly because we don’t use property wrappers like @ObservedObject
anymore with @Observable
.
Example: Using @ObservedObject
Traditionally, when using @ObservedObject
, we create a binding like this:
struct Counter: View {
@ObservedObject var model: Model
var body: some View {
Stepper("\(model.value)", value: $model.value)
}
}
Here, @ObservedObject
provides a projected value ($model
) that allows us to bind to model.value
.
However, if we switch to using the @Observable
macro, the property wrapper (@ObservedObject
) is gone, and we can’t create bindings the same way.
Solution: @Bindable
To solve this, SwiftUI introduces @Bindable
, a property wrapper specifically for creating bindings with @Observable
objects.
Example: Using @Bindable
struct Counter: View {
@Bindable var model: Model
var body: some View {
Stepper("\(model.value)", value: $model.value)
}
}
@Bindable
wraps themodel
, allowing us to access its properties and create bindings using the$
syntax.- Here,
@Bindable
automatically handles bindingmodel.value
to theStepper
.
Simplifying with Inline Bindable
Instead of declaring @Bindable
on the property, we can also create a Bindable
object inline.
Example: Inline Bindable
struct ContentView: View {
var model = Model.shared
var body: some View {
Stepper("\(model.value)", value: Bindable(model).value)
}
}
In this example, we use Bindable(model)
inline, which effectively does the same thing as the @Bindable
wrapper but without the need to declare it as a property wrapper. It allows us to bind model.value
directly.
Dynamic Member Lookup
Dynamic member lookup, which simplifies accessing properties of the bindable object.
When SwiftUI uses @Bindable
, it leverages the _model.projectedValue[dynamicMember: \.value]
syntax behind the scenes to access properties dynamically.
Here’s a version of the Counter
view without the syntactic sugar, showing how the bindings actually work internally:
Example: Rewriting @Bindable
with Full Syntax
struct Counter: View {
var model: Model { _model.wrappedValue } // Access the original model
var _model: Bindable<Model> // Declare a Bindable wrapper around the model
init(model: Model) {
_model = Bindable(wrappedValue: model) // Initialize the Bindable
}
var body: some View {
Stepper("\(model.value)", value: _model.projectedValue[dynamicMember: \.value])
}
}
Here, model.value
is essentially a shortcut for _model.projectedValue[dynamicMember: \.value]
. This dynamic member lookup allows SwiftUI to access properties on model
in a clean way.
Key Takeaways:
- @Observable objects don’t support traditional bindings because they don’t use property wrappers like
@ObservedObject
. - @Bindable is introduced to wrap
Observable
objects and allow bindings. - You can use
@Bindable
as a property wrapper or inline when needed. - SwiftUI uses dynamic member lookup to simplify property access through bindable objects.
This new approach streamlines working with Observable
objects while still providing powerful binding capabilities in SwiftUI.