swiftuiswiftui-list

List Selection not working for custom view in swift


I've been trying to sort out selections of items in a List, and first had to find the simplest possible example, which works. If you click on an item, it is highlighted, and the text box at the bottom shows the selected item.

struct ListTest1: View {
  @State private var selection: String?

  let names = [
      "Cyril",
      "Lana",
      "Mallory",
      "Sterling"
  ]
  
  var body: some View {
    VStack {
      List(names, id: \.self, selection: $selection) { contact in
        Text(contact)
      }
      Text("\(selection ?? "N/A")")
    }
  }
}

But when I try to use a simple custom view with a struct holding data, nothing works.

struct testData: Hashable {
  var name = ""
  var id = UUID()
}

struct ListTest2: View {
  @State private var selection: testData?

  let names = [
    testData(name: "Cyril"),
    testData(name: "Lana"),
    testData(name: "Mallory"),
    testData(name: "Sterling")
  ]
  
  var body: some View {
    VStack {
      List(names, id: \.id, selection: $selection) { contact in
        Text(contact.name)
      }
      Text("\((selection == nil) ? "N/A" : selection!.name)")
    }
  }
}

The item isn't highlighted or selected in the bottom text block. I'm sure there is a simple explanation but I don't understand enough about the internals to figure it out.


Solution

  • The selection stores the ID of the item -- not the item itself. The ID is the default tag given to the item in the List

    This updated code works:

    struct testData: Identifiable, Hashable {
        var name = ""
        var id = UUID()
    }
    
    struct ListTest2: View {
        @State private var selection: testData.ID?
        
        let names = [
            testData(name: "Cyril"),
            testData(name: "Lana"),
            testData(name: "Mallory"),
            testData(name: "Sterling")
        ]
        
        var body: some View {
            VStack {
                List(names, selection: $selection) { contact in
                    Text(contact.name)
                }
                if let item = names.first(where: { $0.id == selection }) {
                    Text(item.name)
                } else {
                    Text("N/A")
                }
            }
        }
    }
    

    Or, if you really wanted the item itself, you could use tag:

    struct testData: Identifiable, Hashable {
        var name = ""
        var id = UUID()
    }
    
    struct ListTest2: View {
        @State private var selection: testData?
        
        let names = [
            testData(name: "Cyril"),
            testData(name: "Lana"),
            testData(name: "Mallory"),
            testData(name: "Sterling")
        ]
        
        var body: some View {
            VStack {
                List(names, selection: $selection) { contact in
                    Text(contact.name).tag(contact)
                }
                if let selection {
                    Text(selection.name)
                } else {
                    Text("N/A")
                }
            }
        }
    }
    

    Note that on the second example, if you had a more complicated model and that model were mutable, if a property changed on the model, the selection would become invalid. By using Identifiable, like the first example, you avoid that issue.