[SC]()

iOS. Apple. Indies. Plus Things.

Using @Binding with @Environment(Object.self)

// Written by Jordan Morgan // Dec 31st, 2023 // Read it in about 2 minutes // RE: SwiftUI

This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.

iOS 17 brought us more changes to how we manage state and interface changes in SwiftUI. Just as we were given @StateObject in iOS 14 to plug some holes - the Observation framework basically changes how we manage state altogether. However, if you’re just moving to it as I am, you may get confused on how bindings work.

Traditionally, with the “old” way, we could snag a binding to an observed object like this:

class Post: ObservableObject {	
	@Published var text: String = ""
}

struct MainView: View {
	@StateObject private var post: Post = .init()

	var body: some View {
		VStack {
			// More views
			WritingView()
		}
		.environmentObject(post)
	}
}

struct WritingView: View {
	@EnvironmentObject private var post: Post

	var body: some View {
		TextField("New post...", text: $post.text)
	}
}

However, with Observation, passing around the post works a little differently. If you’re like me, you might’ve thought getting a binding to mutate the text would look like this:

@Observable
class Post {	
	var text: String = ""
}

struct MainView: View {
	@State private var post: Post = .init()

	var body: some View {
		VStack {
			// More views
			WritingView()
		}
		.environment(post)
	}
}

struct WritingView: View {
	@Environment(Post.self) private var post: Post

	var body: some View {
		// ! Compiler Error !
		TextField("New post...", text: $post.text)  // Cannot find '$post' in scope
	}
}

I haven’t watched the session over Observation in some time, so I was puzzled by this. It turns out, you create a binding directly in the body:

struct WritingView: View {
	@Environment(Post.self) private var post: Post

	var body: some View {
		@Bindable var post = post
		TextField("New post...", text: $post.text)
	}
}

This is mentioned directly in the documentation as it turns out, with an identical example as seen here:

Use this same approach when you need a binding to a property of an observable object stored in a view’s environment.

I don’t know why @Bindable was designed like this, I’m sure there is a technical reason, but as an API consumer it seems counterintuitive. Which is odd, considering all of the ease of use the Observation framework brings. Regardless, another solution proposed in a thread over the issue mentions that you could drop the object through another view:

struct WritingView: View {
	@Environment(Post.self) private var post: Post

	var body: some View {
		PostTextView(post: post)
	}
}

struct PostTextView: View {
	@Bindable var post: Post
	
	var body: some View {
		TextField("New post...", text: $post.text)
	}
}

So, if you get stuck with grabbing a binding to an Observable object - you can use either of these approaches.

Until next time ✌️.

···

Spot an issue, anything to add?

Reach Out.