88

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

203

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.

9
  • 20
    A couple of variations of this syntax: .tag(nil as Fruit?), .tag(Fruit?.none), .tag(Fruit?(nil)), .tag(Optional<Fruit>.none). 🙂
    – markiv
    Commented Oct 29, 2020 at 8:59
  • 7
    This is a major caveat (a compiler warning would be helpful here). I spent at least a couple days tracking this one down. 🤦🏻‍♂️ Commented Jul 25, 2021 at 4:39
  • 2
    Wow... this is 🤯 I feel like this should not be a thing. LOL! Thanks though, this works!
    – Fogmeister
    Commented Jul 15, 2022 at 15:12
  • 3
    Or .tag(Optional(fruit)). Commented Jul 20, 2022 at 9:47
  • 3
    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." Commented Sep 26, 2023 at 16:45
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
    }
}
1

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

EDIT: Thank you for the comments regarding posting links. 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()
    }
}
3
  • 2
    Please don't just post links to an answer. Post the relevant code and the link. That way your answer doesn't disappear and the value that it could have provided is lost.
    – Andrew
    Commented Jul 9, 2020 at 11:41
  • 1
    I appreciate that, gentlemen (@Andrew, @kenny_k). Edited!
    – atdonsm
    Commented Jul 9, 2020 at 15:07
  • What does this answer add over the original answer by @Senseful?
    – GazB
    Commented Apr 21, 2023 at 5:35
-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
  • 2
    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!
    – Grimxn
    Commented Dec 20, 2019 at 13:32
  • 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.
    – kamcma
    Commented Feb 24, 2022 at 20:15
-1

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.

Not the answer you're looking for? Browse other questions tagged or ask your own question.