Non-Selectable Rows in a SwiftUI Picker

With SwiftUI, it’s quite easy to create forms to collect user input. Consider the following.

struct ContentView: View {
    @State var numberOfCookies: Int?
    
    var body: some View {
        NavigationView {
            Form {
                Picker("How many cookies?", selection: $numberOfCookies) {
                    ForEach(1..<10) { number in
                        Text("\(number)")
                            .tag(number as Int?)
                    }
                }
            }
            .navigationTitle("Order")
        }
    }
}

Being in a Form in a NavigationView means our Picker above will navigate to a list on tap by default. We thus get a view that looks like this.

If they tap on the row, they’re shown this list of options.

The Picker will take each view generated in the ForEach and show it as an option.

This is super simple! However, what if we wanted to show some kind of header alongside our various options? Or perhaps we wanted to section the options? Or maybe we’re displaying options from a third-party API that includes such headers or section names in the returned data? To figure that out, let’s first understand how the SwiftUI picker works.

How Picker Works

The above code seems fairly straightforward. I defined a Picker in a hierarchy. It had a title that is displayed in the form. The selection was set as a binding to a local state variable. The trailing closure was my picker content, where I used a ForEach to generate each option.

However, as simple as that might be, you might be asking: What is with the tag?

Each view that is used in the Picker needs to be tagged with the value it represents. In our case, it’s just a number. So I am tagging each generated view so that it corresponds with a valid value.

But why didn’t I just say “.tag(number)“? It’s because it wouldn’t match the type the Picker is looking for, namely the optional Int of the state variable. If I had done that, the Picker would still show each of my numeric options, but none of them would be selectable. Tapping on one wouldn’t change the data source.

This leads to an interesting observation: Any view in the Picker that doesn’t have a tag matching the bound type is thus non-selectable.

Making Picker Headers

With this knowledge in mind, let’s go back to our example. Let’s say we wanted to show that we have a discount if the customer buys more than 4 cookies. How can we do it?

We change our picker like this.

Picker("How many cookies?", selection: $numberOfCookies) {
    Text("Normal Price")
        .font(.headline)
    
    ForEach(1..<5) { number in
        Text("\(number)")
            .tag(number as Int?)
    }
    
    Label("Discount", systemImage: "tag")
        .font(.headline)
    
    ForEach(5..<10) { number in
        Text("\(number)")
            .tag(number as Int?)
    }
}

We now have other views in our picker that are not tagged as options, thus becoming visible yet non-selectable display items. In the above code, we first display “Normal Price” before showing options for quantities up to 4. We then show a more complex view, a Label, before showing the other quantities.

Our view now looks like this.

And just like that, we have support for displaying non-selectable content within our picker.