Using @Binding with @Environment(Object.self)
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 ✌️.