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