How does one enable selections in SwiftUI's List
Asked Answered
P

4

41

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.

Pent answered 21/6, 2019 at 15:17 Comment(3)
Did my answer help? Or did I misunderstand your question?Progestational
I don't know about the OP, but you helped me! I did not realised Set implemented the SelectionManager protocol! Thanks!Razzia
Sorry @piebie, your answer was immensely helpful. Thank you!Pent
P
33

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

Progestational answered 21/6, 2019 at 16:44 Comment(1)
SelectKeeper is not used and not neededCoquelicot
H
31

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)"))
        }
    }
}
Heinrick answered 28/6, 2019 at 22:13 Comment(1)
This definitely seems like the best way to approach this problem. However, any tips on how to avoid the lag between the view displaying and the "switch" into edit mode?Angus
D
22

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://mcmap.net/q/392669/-select-multiple-items-in-swiftui-list. 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

Disaccharide answered 14/7, 2019 at 7:40 Comment(1)
Amazing! Is it possible to add this to a form like the way picker is?Arbitral
A
0

You you need a simpler solution without "check" (√) see here:

Select Multiple Items in SwiftUI List

for my solutions.

Arria answered 24/6 at 7:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.