93

Normally I can display a list of items like this in SwiftUI:

enum Fruit {
    case apple
    case orange
    case banana
}

struct FruitView: View {

    @State private var fruit = Fruit.apple

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit)
            }
        }
    }
}

This works perfectly, allowing me to select whichever fruit I want. If I want to switch fruit to be nullable (aka an optional), though, it causes problems:

struct FruitView: View {

    @State private var fruit: Fruit?

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit)
            }
        }
    }
}

The selected fruit name is no longer displayed on the first screen, and no matter what selection item I choose, it doesn't update the fruit value.

How do I use Picker with an optional type?

0

5 Answers 5

227

The tag must match the exact data type as the binding is wrapping. In this case the data type provided to tag is Fruit but the data type of $fruit.wrappedValue is Fruit?. You can fix this by casting the datatype in the tag method:

struct FruitView: View {

    @State private var fruit: Fruit?

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit as Fruit?)
            }
        }
    }
}

Bonus: If you want custom text for nil (instead of just blank), and want the user to be allowed to select nil (Note: it's either all or nothing here), you can include an item for nil:

struct FruitView: View {

    @State private var fruit: Fruit?

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            Text("No fruit").tag(nil as Fruit?)
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit as Fruit?)
            }
        }
    }
}

Don't forget to cast the nil value as well.

Sign up to request clarification or add additional context in comments.

6 Comments

A couple of variations of this syntax: .tag(nil as Fruit?), .tag(Fruit?.none), .tag(Fruit?(nil)), .tag(Optional<Fruit>.none). 🙂
Or .tag(Optional(fruit)).
This is great. I now have a segmented picker that can start off with no selection. However, I would also like to be able to deselect the current selection by tapping on it. I have tried using onTapGesture but this swallows the whole tap (i.e. the picker no longer operates). I need some way of "calling super", or an alternative approach. Any ideas?
in Xcode15 this approach works fine, but still has this warning in the debug area: "Picker: the selection "nil" is invalid and does not have an associated tag, this will give undefined results."
Thanks! Great answer!!It was frustrating searching for the solution. The '.palette' style example in the Apple Developer documentation does not have this correct. -> developer.apple.com/documentation/swiftui/pickerstyle/palette
|
2

I actually prefer Senseful's solution for a point solution, but for posterity: you could also create a wrapper enum, which if you have a ton of entity types in your app scales quite nicely via protocol extensions.

// utility constraint to ensure a default id can be produced
protocol EmptyInitializable {
    init()
}

// primary constraint on PickerValue wrapper
protocol Pickable {
    associatedtype Element: Identifiable where Element.ID: EmptyInitializable
}

// wrapper to hide optionality
enum PickerValue<Element>: Pickable where Element: Identifiable, Element.ID: EmptyInitializable {
    case none
    case some(Element)
}

// hashable & equtable on the wrapper
extension PickerValue: Hashable & Equatable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func ==(lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id
    }
}

// common identifiable types
extension String: EmptyInitializable {}
extension Int: EmptyInitializable {}
extension UInt: EmptyInitializable {}
extension UInt8: EmptyInitializable {}
extension UInt16: EmptyInitializable {}
extension UInt32: EmptyInitializable {}
extension UInt64: EmptyInitializable {}
extension UUID: EmptyInitializable {}

// id producer on wrapper
extension PickerValue: Identifiable {
    var id: Element.ID {
        switch self {
            case .some(let e):
                return e.id
            case .none:
                return Element.ID()
        }
    }
}

// utility extensions on Array to wrap into PickerValues
extension Array where Element: Identifiable, Element.ID: EmptyInitializable {
    var pickable: Array<PickerValue<Element>> {
        map { .some($0) }
    }
    
    var optionalPickable: Array<PickerValue<Element>> {
        [.none] + pickable
    }
}

// benefit of wrapping with protocols is that item views can be common
// across data sets.  (Here TitleComponent { var title: String { get }})
extension PickerValue where Element: TitleComponent {
    @ViewBuilder
    var itemView: some View {
        Group {
            switch self {
                case .some(let e):
                    Text(e.title)
                case .none:
                    Text("None")
                        .italic()
                        .foregroundColor(.accentColor)
            }
        }
        .tag(self)
    }
}

Usage is then quite tight:

Picker(selection: $task.job, label: Text("Job")) {
    ForEach(Model.shared.jobs.optionalPickable) { p in
        p.itemView
    }
}

Comments

1

I made a public repo here with Senseful's solution: https://github.com/andrewthedina/SwiftUIPickerWithOptionalSelection

Here is the code which answers the question. Copy/paste will do the trick, or clone the repo from the link.

import SwiftUI

struct ContentView: View {
    @State private var selectionOne: String? = nil
    @State private var selectionTwo: String? = nil
    
    let items = ["Item A", "Item B", "Item C"]
    
    var body: some View {
        NavigationView {
            Form {
                // MARK: - Option 1: NIL by SELECTION
                Picker(selection: $selectionOne, label: Text("Picker with option to select nil item [none]")) {
                    Text("[none]").tag(nil as String?)
                        .foregroundColor(.red)

                    ForEach(items, id: \.self) { item in
                        Text(item).tag(item as String?)
                        // Tags must be cast to same type as Picker selection
                    }
                }
                
                // MARK: - Option 2: NIL by BUTTON ACTION
                Picker(selection: $selectionTwo, label: Text("Picker with Button that removes selection")) {
                    ForEach(items, id: \.self) { item in
                        Text(item).tag(item as String?)
                        // Tags must be cast to same type as Picker selection
                    }
                }
                
                if selectionTwo != nil { // "Remove item" button only appears if selection is not nil
                    Button("Remove item") {
                        self.selectionTwo = nil
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

1 Comment

What does this answer add over the original answer by @Senseful?
-1

Why not extending the enum with a default value? If this is not what you are trying to achieve, maybe you can also provide some information, why you want to have it optional.

enum Fruit: String, CaseIterable, Hashable {
    case apple = "apple"
    case orange = "orange"
    case banana = "banana"
    case noValue = ""
}

struct ContentView: View {

    @State private var fruit = Fruit.noValue

    var body: some View {
        VStack{
            Picker(selection: $fruit, label: Text("Fruit")) {
                ForEach(Fruit.allCases, id:\.self) { fruit in
                    Text(fruit.rawValue)
                }
            }
            Text("Selected Fruit: \(fruit.rawValue)")
        }
    }
}

2 Comments

Your comment is fair enough for an enum, but what if it was selection from an array or set where "none of the above" could be a valid choice? @Senseful's answer is then very useful!
Adding a noValue case to an enum is basically reimplementing Optional without using Optional. You'd be better off (syntax support, composability) just using Optional.
-2

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 quite a few hours of making mistakes.

So when I use Jim's technique to create Extensions on SwiftUI Binding 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)) { ... }

OR when using .pickerStyle(.segmented)

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

which sets the index of the segmented style picker to -1 as per the documentation for UISegmentedControl and selectedSegmentIndex.

The default value is noSegment (no segment selected) until the user touches a segment. Set this property to -1 to turn off the current selection.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.