Deselecting item from a picker SwiftUI

Viewed 1024

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

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

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

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.

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