swiftuiios16swiftui-navigationsplitview

NavigationSplitView works with .ID not with Object(struct)


In my project I want to navigate to the detail View in a NavigationSplitView. This however does not work, nothing happens. When I change the selection to the ID property all of it works. I made a very simple example view that demonstrates this (real project is bigger)

This doesn't work

struct ContentView: View {
    var notes = [
        Annotation(title: "Note 1", details: "Some details here", completed: false),
        Annotation(title: "Note 2", details: "Some details here too", completed: true),
        Annotation(title: "Note 3", details: "Some details here also", completed: false)
    ]
    
    @State private var selected: Annotation?
        
    var body: some View {
        NavigationSplitView(columnVisibility: .constant(.doubleColumn)) {
            List (selection: $selected) {
                ForEach(notes) { annotation in
                        Text(annotation.title)
                    }
            }
        } detail: {
            VStack {
                Text(selected?.title ?? "-")
                Text(selected?.details ?? "-")
                //Text(selected?.uuidString ?? "###")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct Annotation: Hashable, Identifiable {
    var id: UUID = UUID()
    var title: String
    var details: String
    var completed: Bool
}

This works

struct ContentView: View {
    var notes = [
        Annotation(title: "Note 1", details: "Some details here", completed: false),
        Annotation(title: "Note 2", details: "Some details here too", completed: true),
        Annotation(title: "Note 3", details: "Some details here also", completed: false)
    ]
    
    @State private var selected: Annotation.ID?
        
    var body: some View {
        NavigationSplitView(columnVisibility: .constant(.doubleColumn)) {
            List (selection: $selected) {
                ForEach(notes) { annotation in
                        Text(annotation.title)
                    }
            }
        } detail: {
            VStack {
                //Text(selected?.title ?? "-")
                //Text(selected?.details ?? "-")
                Text(selected?.uuidString ?? "###")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct Annotation: Hashable, Identifiable {
    var id: UUID = UUID()
    var title: String
    var details: String
    var completed: Bool
}

Any help appreciated!


Solution

  • By default, the ForEach puts a tag on each item with the item's id. So, the selection is updated with that id. If you want the tag to match the entire item, you have to use the tag modifier to explicitly do that:

    List (selection: $selected) {
        ForEach(notes) { annotation in
            Text(annotation.title)
                 .tag(annotation) // <-- Here
        }
    }
    

    Note that this is potentially dangerous, as if your model had a property that changed when you were on the detail view, it would no longer stay selected, since the equality would be broken. So, it's probably best/safest to stick with the id-based selection.