[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 Emerge Tools, the best way to build on mobile.

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.