How to create a binding to an environment object in SwiftUI


Greetings, traveler!

Since Apple introduced the Observation framework in iOS 17, life has become easier, and code has become faster. With the new Observation framework, you can migrate from the ObservableObject protocol to the Observable macro and from the EnviromentObject to the Enviroment. With Observable macro, all properties become observable by default. But what if we want to create bindings to the properties of enviroment objects? Let’s try this out.

Before we delve into the new Observation framework, let’s consider how this was done the ‘old way ‘.

final class Model: ObservableObject {

    @Published var name: String = ""
    
}

struct SwiftUIView: View {
    
    @EnvironmentObject var model: Model

    var body: some View {
        TextField("", text: $model.name)
    }
    
}

Now, let’s rewrite the code with the observation framework. Spoiler: we will get an error.

@Observable
final class Model {

    var name: String = ""
    
}

struct SwiftUIView: View {
    
    @Environment(Model.self) var model
    
    var body: some View {
        TextField("", text: $model.name) // ❌ Cannot find '$model' in scope
    }
    
}

To use @Environment for binding, you can create a Bindable variable. Apple offers the @Bindable property wrapper to bind with observable types.

struct SwiftUIView: View {
    
    @Environment(Model.self) var model
    
    var body: some View {
        
        @Bindable var model = model
        
        TextField("", text: $model.name)
    }
    
}

You can also inject bindable objects like this into child views.

struct SwiftUIView: View {
    
    @Environment(Model.self) var model
    
    var body: some View {
        SwiftUIView2(model: model)
    }
    
}

struct SwiftUIView2: View {
    
    @Bindable var model: Model
    
    var body: some View {
        TextField("", text: $model.name)
    }
    
}

Or use it like this:

struct SwiftUIView: View {
    
    @Environment(Model.self) var model
    
    var body: some View {
        TextField("", text: Bindable(model).name)
    }
    
}

The question is, how much will it cost? If we are talking about performance. The second option looks a bit cheaper.

Custom Bindable Environment

In addition, Iet’s create a custom EnvironmentBindable propertyWrapper to make our code a bit cleaner.

@propertyWrapper
struct EnvironmentBindable<T: Observable & AnyObject>: DynamicProperty {
    
    @Environment(T.self) private var object
    
    var wrappedValue: T {
        _object.wrappedValue
    }
    
    var projectedValue: Bindable<T> {
        @Bindable var wrappedValue = wrappedValue
        return $wrappedValue
    }
    
}

Conclusion

That looks a bit weird, isn’t it? We may get another solution from Apple shortly. But for now, this is the only approved way to use Bindings with Observable objects.

Note

By the way, there is another suspicious API from the Observation framework. Don’t hesitate to check it out here.