macosswiftuiswiftui-navigationlinkswiftui-navigationsplitview

Detail view of NavigationSplitView partially works


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()
}

Solution

  • 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