listselectionswiftui

How does one enable selections in SwiftUI's List


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.


Solution

  • 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