Detail view of NavigationSplitView partially works

206 views Asked by At

I wrote a sample app (for macOS) which contains 3 users. I use NavigationLink for each users in a NavigationSplitView. When NavigationLink of any user is clicked the detail view should show the user name(s) and age in edit mode. It works but only if the first user is clicked. If I click on any user, then the detail view  does not change: it shows the first user data.

I am new with SwiftUI and I am sure I misunderstood something but I have more than 20 years programming experience except SwiftUI. Thanks for any help.

I googled for days, I got many good ideas (especially from stackoverflow), but I could not resolve my problem. Hopefully somebody can explain what I am doing wrong.

// Create users array
class Users: ObservableObject {
    @Published var users: [UserItem] = [
        UserItem(id: 0, nickName: "Nick1", familyName: "Family1", firstName: "First1", age: 42),
        UserItem(id: 1, nickName: "Nick2", familyName: "Family2", firstName: "First2", age: 39),
        UserItem(id: 2, nickName: "Nick3", familyName: "Family3", firstName: "First3", age: 35)
    ]
}

// Define user properties
class UserItem: Identifiable, ObservableObject {
    @Published var id:Int
    @Published var nickName:String
    @Published var familyName:String
    @Published var firstName:String
    @Published var age:Int
    
    init(id:Int, nickName:String, familyName:String, firstName:String, age:Int) {
        self.id = id
        self.age = age
        self.familyName = familyName
        self.firstName = firstName
        self.nickName = nickName
    }
    
    func fullName() -> String {
        return "\(self.familyName) \(self.firstName) (\(self.nickName)) \(self.age)"
    }
}

// Show users and their detail
struct Test2: View {
    @StateObject var usersData = Users()
    
    var body: some View {
        NavigationSplitView {
            List {
                ForEach(usersData.users) { user in
                    NavigationLink {
                        UserDetailsView(user: user)
                    } label:{
                        Text(user.fullName())
                    }
                }
                Spacer()
            }
        } detail: {
            Text("Select a user to edit.")
        }
        .padding()
        .navigationTitle("Users")
    }
}

// Show user detail
struct UserDetailsView: View {
    @StateObject var user: UserItem
    
    var body: some View {
        VStack {
            TextField("Family name", text: $user.familyName)
            TextField("First name", text: $user.firstName)
            TextField("Nick name", text: $user.nickName)
            Stepper("Age: \(user.age)", value: $user.age)
            Spacer()
            
            // Check user property (live update))
            Text(user.fullName())
        }.padding()
    }
}

#Preview {
    Test2()
}
1

There are 1 answers

1
workingdog support Ukraine On BEST ANSWER

Try this approach using a struct UserItem: Identifiable (nesting ObservableObject can be problematic) and a @Binding in UserDetailsView to allow editing of your data (two way binding).

For a more detail explanantion, see this official link, it gives you some good examples of how to manage data in your app: monitoring data
Also binding in ForEach

struct UserItem: Identifiable {  // <--- here
 var id:Int
var nickName:String
var familyName:String
 var firstName:String
 var age:Int
    
    init(id:Int, nickName:String, familyName:String, firstName:String, age:Int) {
        self.id = id
        self.age = age
        self.familyName = familyName
        self.firstName = firstName
        self.nickName = nickName
    }
    
    func fullName() -> String {
        return "\(self.familyName) \(self.firstName) (\(self.nickName)) \(self.age)"
    }
}

// Show users and their detail
struct ContentView: View {
    @StateObject var usersData = Users()
    
    var body: some View {
        NavigationSplitView {
            List {
                ForEach($usersData.users) { $user in  // <--- here
                    NavigationLink {
                        UserDetailsView(user: $user)  // <--- here
                    } label:{
                        Text(user.fullName())
                    }
                }
                Spacer()
            }
        } detail: {
            Text("Select a user to edit.")
        }
        .padding()
        .navigationTitle("Users")
    }
}

// Show user detail
struct UserDetailsView: View {
    @Binding var user: UserItem   // <--- here
    
    var body: some View {
        VStack {
            TextField("Family name", text: $user.familyName)
            TextField("First name", text: $user.firstName)
            TextField("Nick name", text: $user.nickName)
            Stepper("Age: \(user.age)", value: $user.age)
            Spacer()
            // Check user property (live update))
            Text(user.fullName())
        }.padding()
    }
}

On MacOS 14.2, using Xcode 15.1, tested on real ios17 devices (not Previews), MacCatalyst and MacOS 14.2. It could be different on older systems. Note it can also work using class UserItem: ObservableObject.

The main problem you encountered is because you use @StateObject var user: UserItem. With this, the "SwiftUI creates a new instance of the model object only once during the lifetime of the container that declares the state object.". This is not what you want. See: https://developer.apple.com/documentation/swiftui/stateobject