At this point, we're going to have to roll our own.Note: this implementation has a bug which means that only the Text
will cause a selection to occur. It is possible to do this with Button
but because of the change in Beta 2 that removed borderlessButtonStyle()
it looks goofy, and I haven't figured out a workaround yet.
struct Person: Identifiable, Hashable {let id = UUID()let name: String}var demoData = [Person(name: "Phil Swanson"), Person(name: "Karen Gibbons"), Person(name: "Grant Kilman"), Person(name: "Wanda Green")]struct SelectKeeper : SelectionManager{var selections = Set<UUID>()mutating func select(_ value: UUID) {selections.insert(value)}mutating func deselect(_ value: UUID) {selections.remove(value)}func isSelected(_ value: UUID) -> Bool {return selections.contains(value)}typealias SelectionValue = UUID}struct SelectionDemo : View {@State var selectKeeper = Set<UUID>()var body: some View {NavigationView {List(demoData) { person inSelectableRow(person: person, selectedItems: self.$selectKeeper)}.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))}}}struct SelectableRow: View {var person: Person@Binding var selectedItems: Set<UUID>var isSelected: Bool {selectedItems.contains(person.id)}var body: some View {GeometryReader { geo inHStack {Text(self.person.name).frame(width: geo.size.width, height: geo.size.height, alignment: .leading)}.background(self.isSelected ? Color.gray : Color.clear).tapAction {if self.isSelected {self.selectedItems.remove(self.person.id)} else {self.selectedItems.insert(self.person.id)}}}}}
Edit Mode
As mentioned in a previous answer you can add this in edit mode. This means that the user will have to press the edit button at some point to select rows. This is useful if you want to have a view state and an edit state for your list.
var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]struct SelectionDemo : View {@State var selectKeeper = Set<String>()var body: some View {NavigationView {List(demoData, id: \.self, selection: $selectKeeper){ name inText(name)}.navigationBarItems(trailing: EditButton()).navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))}}}
Constant Edit Mode
You can also simply keep edit mode always on. SwiftUI has environment modifiers, that allow you to manually control any environment variables. In this case we wan to control the editMode
variable.
var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]struct SelectionDemo : View {@State var selectKeeper = Set<String>()var body: some View {NavigationView {List(demoData, id: \.self, selection: $selectKeeper){ name inText(name)}// the next line is the modifier.environment(\.editMode, .constant(EditMode.active)).navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))}}}
Rather than using edit mode, I’d just update the row on the basis of the model, and toggle a boolean in the model when the row is tapped as suggested by https://stackoverflow.com/a/57023746/1271826. Perhaps something like:
struct MultipleSelectionRow<RowContent: SelectableRow>: View {var content: Binding<RowContent>var body: some View {Button(action: {self.content.value.isSelected.toggle()}) {HStack {Text(content.value.text)Spacer()Image(systemName: content.value.isSelected ? "checkmark.circle.fill" : "circle")}}}}
Where
protocol SelectableRow {var text: String { get }var isSelected: Bool { get set }}
Then you can do things like:
struct Person: Hashable, Identifiable, SelectableRow {let id = UUID().uuidStringlet text: Stringvar isSelected: Bool = false}struct ContentView : View {@State var people: [Person] = [Person(text: "Mo"),Person(text: "Larry"),Person(text: "Curly")]var body: some View {List {ForEach($people.identified(by: \.id)) { person inMultipleSelectionRow(content: person)}}}}
Yielding: