How to make a list with a filter field in SwiftUI on Mac

I'm trying to build a dialog where the user can select from one or more of (currently) four lists. Since each list is structured similarly, I'm building a view that will be used by each of the lists. The lists can potentially have thousands of entries, so I'd like to allow the user to filter the list by typing into a search field (which in SwiftUI would be just a TextField) and only showing the matching entries. This is all working with the code below, but I can't select a row of the list.

Code Block Swift
struct RegistryEntry: Equatable, Identifiable, Hashable {
var id = UUID()
var code = ""
var name = ""
var other = ""
}
struct RegistryPicker: View {
@State var sourceList: [RegistryEntry]
@State var searchString = ""
@State var selectedCode: String
@State private var selectedItem: RegistryEntry?
var body: some View {
HStack() {
VStack() {
TextField("Filter", text: $searchString)
Spacer()
}
List(searchString == "" ? sourceList :
sourceList.filter { $0.name.localizedCaseInsensitiveContains(searchString) },
selection: $selectedItem) { entry in
Text(entry.name)
}
}
}


I'm still wrapping my mind around the paradigm shift to SwiftUI, so I may well be missing something obvious.

Further down the track, I need to pass the selected item up to the dialog, and preferably send an initial selection to the list. I assume that is done by making the selection a binding to the appropriate state variable in the enclosing view and setting the selection, presumably in a .onAppear clause. Any pointers there would be welcome, too!

Accepted Reply

I eventually figured out that the important thing I was missing was "id: \.self" in the List statement. I'd understood the documentation to say that wasn't necessary if the content conformed to Identifiable, but I was mistaken, and you need to specify the identifier.

Your solution is interesting, and worth thinking about as I continue to work on the project.
Post marked as downvoted Up vote reply of Mussau Down vote reply of Mussau
Post marked as solved

Replies

To make a List selectable, the binding must be to the id of the items, in your case UUID. So, you would need @State private var selectedItem: UUID?

In MacOS, there's no Selection marker (as there is in iOS), so clicking on a row in a selectable list in MacOS highlights the row. At that point the ID of the row (Item) is in selectedItem as the UUID. To do anything further with the selected Item (row) you would need to retrieve it from the sourceList by filtering on the selected UUID.

I normally create a sequential Integer ID, starting from 0, when setting up the source data for a list so that I can use that as an index to the source data, provided I don't ever delete any item(s) - otherwise I would have to filter on the integer ID.

I also usually use a singleton, class based, data model that is an Observable Object with Published vars and perform all data processing in that model, with changes reflected in the SwiftUI views by a binding to the shared Data Model. For your example the SwiftUI View would be:
  • *** In my example, the user can select an Item without searching, just by clicking on a row.

struct ContentView: View {
    @ObservedObject var dataModel = DataModel.shared
    var body: some View {
        VStack() {
            TextField("Filter", text: $dataModel.searchString)
            Spacer()
            List(dataModel.filteredList, selection: $dataModel.selectedEntry) { entry in
                HStack{
                    Text(entry.code)
                    Text(entry.name)
                    Text(entry.other)
                }
            }
            Text("Selected Item is " + String(dataModel.selectedEntry ?? -1)). // negative means no current selection
        }
    }
}

The Data Model would be:

struct RegistryEntry: Identifiable, Hashable {
    var id = 0
    var code = ""
    var name = ""
    var other = ""
}

class DataModel : ObservableObject {
    static let shared = DataModel()
    var sourceList = [RegistryEntry]()
    @Published var searchString = "" {
        didSet {
            if searchString == "" {
                filteredList = sourceList
                return
            }
            filteredList = sourceList.filter { $0.name.localizedCaseInsensitiveContains(searchString) }
        }
    }
    @Published var selectedEntry : Int?
    @Published var filteredList = [RegistryEntry]()
    init() {
      // set up sourceList by hardcoding or importing
// then set initial filtered list to source list
        filteredList = sourceList
    }
}

Regards, Michaela
I eventually figured out that the important thing I was missing was "id: \.self" in the List statement. I'd understood the documentation to say that wasn't necessary if the content conformed to Identifiable, but I was mistaken, and you need to specify the identifier.

Your solution is interesting, and worth thinking about as I continue to work on the project.
Post marked as downvoted Up vote reply of Mussau Down vote reply of Mussau
Post marked as solved
Now you mention it Mussau, I vaguely remember having problems with UUID as the List's ID in iOS apps. The integer ID approach (as per my example) works fine without specifically specifying the id: parameter in List - although sometimes the compiler complains in iOS if the View is complex, then I have to specify. It looks like the UUID situation might be a bug, unless it's because the UUID is automatically generated by the system whereas my Int isn't.

Anyhow, well done!

Good luck and best wishes, Michaela