Deselect Items from SwiftUI List on macOS with single click
Asked Answered
T

2

1

I'm developing a macOS App with a List View with selectable rows. As there is unfortunately no editMode

enter image description here

on macOS, Single Click Selection of Cells is possible, but deselecting an already selected Cell doing the same does nothing. The only option to deselect the Cell is to CMD + Click which is not very intuitive.
Minimum Example:

struct RowsView: View {
    
    @State var selectKeeper: String?
    
    let rows = ["1", "2", "3", "4", "5", "6", "7", "8"]
    
    var body: some View {
        List(rows, id: \.self, selection: $selectKeeper) { row in
            Text(row)
        }
    }
}

struct RowsView_Previews: PreviewProvider {
    static var previews: some View {
        RowsView()
    }
}

Clicking Row Nr 3 with a Single Click or even double Click does nothing and the row stays selected.
Clicking Row Nr 3 with a Single Click or even double Click does nothing.


Attaching Binding directly
I have tried to attach the Binding directly as described in the excelent answer for a Picker here, but this does not seem to work for List on macOS:

...
    var body: some View {
        List(rows, id: \.self, selection: Binding($selectKeeper, deselectTo: nil)) { row in
            Text(row)
        }
    }
...

public extension Binding where Value: Equatable {
                
    init(_ source: Binding<Value>, deselectTo value: Value) {
        self.init(get: { source.wrappedValue },
                  set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
        )
    }
}

Any ideas on how single click deselect can be made possible without rebuilding the selection mechanism?

For the record: XCode 13.2.1, macOS BigSur 11.6.2

Tatia answered 16/1, 2022 at 7:34 Comment(2)
To confirm, do you want single selection or multiple selection of cells/rows?Brana
Multi Selection, but i bootstrapped the Example to make it easier. It is not working in either wayTatia
F
2

We can block default click handling by using own gesture and manage selection manually.

Here is demo of possible approach. Tested with Xcode 13.2 / macOS 12.1

demo

struct RowsView: View {

    @State var selectKeeper: String?

    let rows = ["1", "2", "3", "4", "5", "6", "7", "8"]

    var body: some View {
        List(rows, id: \.self, selection: $selectKeeper) { row in
            Text(row)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                .contentShape(Rectangle())      // handle click row-wide
                .listRowInsets(EdgeInsets())    // remove default edges
                .onTapGesture {
                    selectKeeper = selectKeeper == row ? nil : row   // << here !!
                }
                .padding(.vertical, 4)          // look&feel like default
        }
    }
}
Fennie answered 16/1, 2022 at 8:33 Comment(1)
Almost perfect, thank you, I have colored the cells so that the missing, non clickable part is easy to see: i.sstatic.net/eFldR.jpg Unfortunatelly this is the hardest part to fit as it is beyond the frame of the cells.Tatia
B
0

I refer to this answer to a SO post titled "Select Multiple Items in a SwiftUI List".

From this answer I generated code to select multiple items in a list.

In my sample code I use a checkmark to illustrate whether a row is selected, but you could change this to suit your needs.

You'll need to change your @State wrapper to a Set, because you confirmed that you'll need to select a group of items. (An aside, Apple recommends that you mark @State property wrappers as private so as to reinforce their intended use locally.)

@State private var selectedItems: Set<String>?
let rows = ["1", "2", "3", "4", "5", "6", "7", "8"]
var body: some View {
    List(rows, id: \.self) { item in            
        RowSelectable(selectedItems: $selectedItems, rowItem: item)
    }
}

where RowSelectable is...

struct RowSelectable: View {
    @Binding var selectedItems: Set<String>?
    var rowItem: String
    var isSelected: Bool {
        return selectedItems?.contains(rowItem) == true
    }
    var body: some View {
        HStack {
            Text(rowItem)
            if selectedItems?.contains(rowItem) == true {
                Spacer()
                Image(systemName: "checkmark")
            }
        }
        .onTapGesture(count: 1) {
            if self.isSelected {
                selectedItems!.remove(rowItem)
            }
            else {
                selectedItems!.insert(rowItem)
            }
        }
    }
}

I haven't tested this yet, so let me know if it doesn't work and I'll check in Xcode.

Brana answered 17/1, 2022 at 21:2 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.