Compile issue with optional `@Observable` object

i'm having trouble modifying an optional environment object.
i'm using the .environment modifier to pass along an optional object to other views. to access it in other views, i have to get it through an @Environment property wrapper. but i can't modify it even if i redeclare it in the body as @Bindable. here's an example code:

@main
struct MyApp: App {
    @State private var mySession: MySession?

    var body: some Scene {
        HomeScreen()
            .environment(mySession)
    }
}

now for the HomeScreen:

struct HomeScreen: View {
    @Environment(MySession.self) private var mySession: MySession?

    var body: some View {
        @Bindable var mySession = mySession
        
        Button {
            mySession = MySession()
        } label: {
            Text("Create Session")
        }
    }
}

an error shows up in the @Bindable declaration saying 'init(wrappedValue:)' is unavailable: The wrapped value must be an object that conforms to Observable . but MySession is declared as @Observable. in fact it works just fine if i don't make the environment optional, but i have to setup MySession in the root of the app, which goes against the app flow.

Hi @moriso, the main problem is that the "wrapped value" the error is referring to is an Optional<MySession>, and Optional does not conditionally conform to Observable. So the error is correct that the thing you are sticking in @Bindable does not conform to Observable.

But even if that did work, the other problem is that mySession = MySession() wouldn't do what you expect it to do. That would only be mutating the @Bindable property, but that would have no affect on the @Environment property. In fact, @Environment's wrappedValue is get-only, and so you can never mutate the environment in that way. The only way to change the environment for a view hierarchy is through the .environment view modifier.

But, I think there is an alternative way to do this that would probably be better in the long run. I would suggest making MySession non-optional when accessed through the environment, and instead put the optional state inside MySession. Then you can mutate it all you want from any view, and it should work exactly as you expect.

thank you so much for the explanation, brandon! it makes sense that the @Bindable wouldn't propagate the change up if it was holding an optional value.
i did what you suggested -- actually, i put an optional MySession variable inside a class called AppSession, and used a non-optional AppSession as the environment object. in the app i'm working on, there's semantic value on MySession being nil or not, so sticking it to that logic made everything work as expected. thank you!

1 Like