Deselecting item from a picker SwiftUI
Asked Answered
A

4

5

I use a form with a picker, and everything works fine (I am able to select an element from the picker), but I cannot deselect it. Does there exist a way to deselect an item from the picker? Thank you!

enter image description here

Picker(selection: $model.countries, label: Text("country")) {
                        ForEach(model.countries, id: \.self) { country in
                            Text(country!.name)
                                .tag(country)
                        }
                    }
Alfred answered 27/1, 2021 at 17:45 Comment(0)
D
1

First of, we can fix the selection. It should match the type of the tag. The tag is given Country, so to have a selection where nothing might be selected, we should use Country? as the selection type.

It should looks like this:

struct ContentView: View {
    
    @ObservedObject private var model = Model()
    @State private var selection: Country?
    
    var body: some View {
        NavigationView {
            Form {
                Picker(selection: $selection, label: Text("country")) {
                    ForEach(model.countries, id: \.self) { country in
                        Text(country!.name)
                            .tag(country)
                    }
                }
                
                Button("Clear") {
                    selection = nil
                }
            }
        }
    }
}

You then just need to set the selection to nil, which is done in the button. You could set selection to nil by any action you want.

Dreddy answered 27/1, 2021 at 18:1 Comment(0)
P
9

To deselect we need optional storage for picker value, so here is a demo of possible approach.

Tested with Xcode 12.1 / iOS 14.1

demo

struct ContentView: View {
    @State private var value: Int?
    var body: some View {
        NavigationView {
            Form {
                let selected = Binding(
                    get: { self.value },
                    set: { self.value = $0 == self.value ? nil : $0 }
                )
                Picker("Select", selection: selected) {
                    ForEach(0...9, id: \.self) {
                        Text("\($0)").tag(Optional($0))
                    }
                }
            }
        }
    }
}
Printer answered 27/1, 2021 at 18:0 Comment(1)
This solution works like a charm if you don"t want a "Clear" button on your interfaceErsatz
U
4

I learned almost all I know about SwiftUI Bindings (with Core Data) by reading this blog by Jim Dovey. The remainder is a combination of some research and many hours of making mistakes.

So when I combine Jim's technique to create Extensions on SwiftUI Binding with Asperi's answer, then we end up with something like this...

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 }
        )
    }
}

Which can then be used throughout your code like this...

Picker("country", selection: Binding($selection, deselectTo: nil)) { ... }

OR

Picker("country", selection: Binding($selection, deselectTo: someOtherValue)) { ... }
Umbrageous answered 18/7, 2021 at 10:46 Comment(3)
Although i think its a brilliant answer and very nice approach, it is not working for a List() in SwiftUI. The set() is never called, when a row is already selected.Hedge
@Hedge in my projects this solution works in every scenario with minor changes as needed. The OP asked about deselecting from a picker, but from what you’ve written, your case sounds different. Consider asking a new question and let me know.Umbrageous
Yes you are absolutely right. Just done so here: https://mcmap.net/q/1922521/-deselect-items-from-swiftui-list-on-macos-with-single-click/5392813 Appreciate any ideas on thisHedge
D
1

First of, we can fix the selection. It should match the type of the tag. The tag is given Country, so to have a selection where nothing might be selected, we should use Country? as the selection type.

It should looks like this:

struct ContentView: View {
    
    @ObservedObject private var model = Model()
    @State private var selection: Country?
    
    var body: some View {
        NavigationView {
            Form {
                Picker(selection: $selection, label: Text("country")) {
                    ForEach(model.countries, id: \.self) { country in
                        Text(country!.name)
                            .tag(country)
                    }
                }
                
                Button("Clear") {
                    selection = nil
                }
            }
        }
    }
}

You then just need to set the selection to nil, which is done in the button. You could set selection to nil by any action you want.

Dreddy answered 27/1, 2021 at 18:1 Comment(0)
C
0

If your deployment target is set to iOS 14 or higher -- Apple has provided a built-in onChange extension to View where you can deselect your row using tag, which can be used like this instead (Thanks)

Picker(selection: $favoriteColor, label: Text("Color")) {
    // ..
}
.onChange(of: favoriteColor) { print("Color tag: \($0)") }
Chare answered 27/1, 2021 at 18:4 Comment(1)
Update: tried and it doesn't work. If the user selects the same value, the "favoriteColor" doesn't change so the .onChange will not be triggeredAlfred

© 2022 - 2024 — McMap. All rights reserved.