How does one enable selections in SwiftUI's List

32,436

Solution 1

Depending on what you want, there are two ways to do this:

If you want to do this in "Edit mode":

You must enable "Edit mode" on the list before a selection matters. From the interface for List:

    /// Creates an instance.
    ///
    /// - Parameter selection: A selection manager that identifies the selected row(s).
    ///
    /// - See Also: `View.selectionValue` which gives an identifier to the rows.
    ///
    /// - Note: On iOS and tvOS, you must explicitly put the `List` into Edit
    /// Mode for the selection to apply.
    @available(watchOS, unavailable)
    public init(selection: Binding<Selection>?, content: () -> Content)

You do that by adding an EditButton to your view somewhere. After that, you just need to bind a var for something that implements SelectionManager(you don't need to roll your own here :D)

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.identified(by: \.self), selection: $selectKeeper){ name in
                Text(name)
            }
            .navigationBarItems(trailing: EditButton())
            .navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
        }
    }
}

This approach looks like this: enter image description here

If you don't want to use "Edit mode":

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 in
                SelectableRow(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 in
            HStack {
                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)
                }
            }
        }
    }
}

enter image description here

Solution 2

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 in
                Text(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 in
                Text(name)
            }
// the next line is the modifier
            .environment(\.editMode, .constant(EditMode.active))
            .navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
        }
    }
}

Solution 3

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().uuidString
    let text: String
    var 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 in
                MultipleSelectionRow(content: person)
            }
        }
    }
}

Yielding:

enter image description here

Share:
32,436
Rumbles
Author by

Rumbles

Updated on July 21, 2022

Comments

  • Rumbles
    Rumbles almost 2 years

    I am trying to create a simple multiple selection List with SwiftUI. I am unable to make it work.

    List takes a second argument which is a SelectionManager, so I tried creating a concrete implementation of one. But, it never gets called and the rows never highlight.

    import SwiftUI
    
    var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "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 = SelectKeeper()
    
        var body: some View {
            NavigationView {
                List(demoData.identified(by: \.self)){ name in
                    Text(name)
                }
                    .navigationBarTitle(Text("Selection Demo"))
            }
        }
    }
    
    #if DEBUG
    struct SelectionDemo_Previews : PreviewProvider {
        static var previews: some View {
            SelectionDemo()
        }
    }
    #endif
    

    Code runs fine but rows don't highlight and the SelectionManager code is never called.