Passing ObservableObject to View within NavigationView and editing it

I'm trying to create a simple NavigationView showing a list of users and be able to edit them in a child view, while using ObservableObject. The only way I could the ObservedObjects to automatically update between the 2 views is to pass the entire users collection from the parent to the child View so that I could call self.users.objectWillChange.send().

This seems inefficient. Is there a better way to do this?

Model Classes


Code Block swift
class User: Identifiable, ObservableObject
{
    let id = UUID()
    @Published var name: String
    init(name: String) { self.name = name }
}

Code Block swift
class Users: ObservableObject
{
    @Published var users: [User]
    init() {
        self.users = [
            User(name: "John Doe"),
            User(name: "Jane Doe"),
        ]
    }
}


Views

Code Block swift
struct TestNavigationView: View {
    @ObservedObject var users = Users()
    var body: some View {
        NavigationView {
            List(self.users.users) { user in
                NavigationLink(destination:
UserDetail(user: user,
users: self.users)
                ) {
                Text(user.name)
                }
            }
        }.navigationBarTitle("Users")
    }
}

Code Block swift
struct UserDetail: View {
    @ObservedObject var user: User
    @ObservedObject var users: Users
    var body: some View {
        VStack {
            TextField("Enter Name", text: $user.name)
            Button(action: {
                self.users.objectWillChange.send()
            }, label: { Text("Save") })
        }
    }
}


Answered by OOPer in 622512022
If you want to utilize the feature creating Binding from @ObservedObject, you may need to write your TestNavigationView like this:
Code Block
struct TestNavigationView: View {
@ObservedObject var users = Users()
var body: some View {
NavigationView {
List(self.users.users.indices) { index in
NavigationLink(destination:
UserDetail(user: self.$users.users[index])
) {
Text(self.users.users[index].name)
}
}
}.navigationBarTitle("Users")
}
}

This code passes Binding<User> (whole User, not only Binding<String> representing User.name) to UserDetail. Isn't it good for future extension, like User hold other properties than name edited in detail view.

With the above change, your UserDetail would become like this:
Code Block
struct UserDetail: View {
@Environment(\.presentationMode) var presentationMode
@Binding var user: User
var body: some View {
VStack {
TextField("Enter Name", text: $user.name)
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("Save") })
}
}
}


hi,

i'll offer three suggestions.
  • there's no need (or there may be, but not in this example) for User to be a class. make it a struct and kill the ObservableObject protocol.

Code Block
struct User: Identifiable {
let id = UUID()
var name: String
init(name: String) { self.name = name }
}
  • treat the Users object as a viewModel, and when a change is made to a user's name, provide a method for the Users object to do that.

Code Block
class Users: ObservableObject {
@Published var users: [User]
init() {
self.users = [
User(name: "John Doe"),
User(name: "Jane Doe")
]
}
func updateUsername(for user: User, to newName: String) {
if let index = users.firstIndex(where: { $0.id == user.id }) {
users[index].name = newName
}
}
}
  • and treat the UserDetailView to not be a live edit, but one where you have parameters users and user coming in; you offload the name you want to edit when the view comes on screen; and when you save the edit, ask the Users object to make that change for you. something like this (with the Save button now also dismissing the view).

Code Block
struct UserDetail: View {
@Environment(\.presentationMode) var presentationMode
var user: User
var users: Users
@State private var username: String = ""
var body: some View {
VStack {
TextField("Enter Name", text: $username)
Button(action: {
self.users.updateUsername(for: self.user, to: self.username)
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("Save") })
}
.onAppear { self.username = self.user.name }
}
}

that should do it. when the Users object updates the user's name in its array, your TestNavigationView will know about it.

hope that helps,
DMG
Thank you so much!!! Your three suggestions were very helpful, and the save button dismissing the view was a nice bonus!

While this is a major improvement to my original approach, I'm wondering if line 8 below can be simplified to use @Binding and just pass user.username
Code Block language
struct TestNavigationView: View {
    @ObservedObject var users = Users()
    var body: some View {
        NavigationView {
            List(self.users.users) { user in
                NavigationLink( destination:
UserDetail(user: user, users: users, username: user.name)) {
                    Text(user.name)
                }
            }
        }.navigationBarTitle("Users")
    }
}

User Detail would look like this:
Code Block swift
struct UserDetail: View {
    @Environment(\.presentationMode) var presentationMode
    @Binding var username: String
    var body: some View {
        VStack {
            TextField("Enter Name", text: $username)
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: { Text("Save") })
        }
    }
}

I noticed others in the forum using this approach (example) but it being contained within a List created an error I couldn't get past:

Cannot convert value of type 'String' to expected argument type 'Binding<String>'

When I change line 8 to below
Code Block swift
struct TestNavigationView: View {
    @ObservedObject var users = Users()
    var body: some View {
        NavigationView {
            List(self.users.users) { user in
                NavigationLink( destination:
UserDetail(username: user.name)) {
                    Text(user.name)
                }
            }
        }.navigationBarTitle("Users")
    }
}


Thanks again!
Accepted Answer
If you want to utilize the feature creating Binding from @ObservedObject, you may need to write your TestNavigationView like this:
Code Block
struct TestNavigationView: View {
@ObservedObject var users = Users()
var body: some View {
NavigationView {
List(self.users.users.indices) { index in
NavigationLink(destination:
UserDetail(user: self.$users.users[index])
) {
Text(self.users.users[index].name)
}
}
}.navigationBarTitle("Users")
}
}

This code passes Binding<User> (whole User, not only Binding<String> representing User.name) to UserDetail. Isn't it good for future extension, like User hold other properties than name edited in detail view.

With the above change, your UserDetail would become like this:
Code Block
struct UserDetail: View {
@Environment(\.presentationMode) var presentationMode
@Binding var user: User
var body: some View {
VStack {
TextField("Enter Name", text: $user.name)
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("Save") })
}
}
}


Thank you!!! That's the simplification I was looking for.
Passing ObservableObject to View within NavigationView and editing it
 
 
Q