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.

Using a Binding in SwiftUI – Simple Example

In my original post on Bindings in SwiftUI, I had mentioned when we might want to use a binding:

You would use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.

I showed an example of how we could bind one of our local @State properties to a Toggle. The Toggle could update the state of our property through it.

We can use other system controls, such as TextField or Picker, to also update our local variables. But what if we want to have another one of our views update a variable owned by another? Let’s solve a simple example and, in doing so, we’ll see when we will make use of @Binding.

I have a simple view that displays my name. I am storing it locally as a variable.

struct NameDisplayView: View {
    var name: String = "unknown"
    
    var body: some View {
        Text("Your name is \(name)")
    }
}

It simply takes the text there and outputs "Your name is unknown". It works, but I want to be able to update that name so I can have it show me and not assume I have no name.

Well, we know how we can bind to something from our original post. So let’s make a view where we can change some text.

struct NameChangeView: View {
    @State var text: String
    
    var body: some View {
        TextField("Type Here", text: $text)
    }
}

So far, this is a simple example. If we loaded up NameChangeView, we could tap into the TextField and, as we type, text would be updated. Why? Because we have it bound to the text property on TextField, which is of type Binding<String>. So as the TextField updates the text, our view can get the latest values.

But let’s say we want to present this view as a sheet, let the user update the text, and then send it back to our original view. Can we do it? Yes!

What do we need to change so that our NameChangeView can pass changes back from itself to another view? All we have to do is change @State to @Binding. With that change, we now have two way communication via that variable.

Let’s add a little styling and a way to dismiss the view. That gives us our final form:

struct NameChangeView: View {
    @Environment(\.presentationMode) var presentationMode
    @Binding var text: String
    
    var body: some View {
        TextField("Type Here", text: $text, onCommit: { self.presentationMode.wrappedValue.dismiss() })
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .padding()
    }
}

Now, how can we use this in our original view?

We’ll add a button to trigger a sheet. This happens by binding a local Bool to the .sheet(isPresented:) modifier. Lastly, we add our NameChangeView as what is being presented modally as a sheet. We pass our local name variable as a binding to that view, so it becomes $name.

struct NameDisplayView: View {
    @State var name: String = "unknown"
    @State var showNameChange = false
    
    var body: some View {
        VStack {
            Text("Your name is \(name)")
            Divider()
            Button("Change") {
                self.showNameChange = true
            }
        }
        .sheet(isPresented: $showNameChange) {
            NameChangeView(text: self.$name)
        }
    }
}

Now, we can tap on “Change”, which will present our text field. We can tap on it, change our name, hit “Return”, and be taken back to our original view but with our name in place.

Bindings might seem like a new concept to iOS developers, but they open up a lot of possibilities in Swift. Explore various ways that you can use bindings to communicate between views. You’ll find them very useful, especially in cases of user input as we saw in our example.

You can find the example code in this Github Gist.

Bindings in SwiftUI

Property wrappers were one of the recent additions to Swift. SwiftUI makes use of several very useful property wrappers. One of them is @Binding. The @Binding property wrapper is really just a convenience for the Binding<T> type.

But before we go into using bindings, what are bindings?

Per Apple’s documentation, a binding connects a property to a source of truth stored elsewhere, instead of storing data directly. You would use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.

In SwiftUI, you’ll find this to be a fairly common pattern, especially with built in input fields.

Binding Example

Let’s say we want a view with a switch for the user to interact with. How do we build it?

struct BindingExample: View {
    @State var switchIsOn = false

    var body: some View {
        Form {
            Toggle(isOn: $switchIsOn) {
                Text("Switch 1")
            }
        }
    }
}

First, we need a mutable variable to hold the state of our switch. In this case, we have a Bool, named switchIsOn. We have to use the @State wrapper so it can be modified in our view.

Next, when we build our Toggle, we need to provide a binding to a property that will hold the state of the toggle. When we pass in the variable, we use the dollar sign to denote that we’re binding to it. So our view holds the current state of switchIsOn, while the Toggle has a binding internally. When the state of the Toggle flips, it is propagated through $switchIsOn back into our view.

Creating a manual binding

If we again look at Apple’s description of a binding, we see that it involves a two-way connection. The binding needs to know the value to get, and where to set the new value when it is changed.

With that knowledge, we can look at how Binding workings. Let’s look at our example again. This time, we’re going to make a binding ourselves.

struct BindingExample: View {
    @State var switchIsOn = false 

    var body: some View {
        Form {  
            Toggle(isOn: Binding(get: {
                self.switchIsOn
            }, set: { newValue in
                self.switchIsOn = newValue
            }) ) {
                Text("Also Switch 1")
            }
        }
    }
}

Binding has an initializer that takes a couple of arguments. One, with the get label, is a closure explaining where the binding gets its value. In our example, this is our switchIsOn variable. The other, set, is given the new value for the binding. This gives us the opportunity to do something with it now that it has changed. In our case, we’re setting switchIsOn to the new value.

In its most basic form, Binding opens up a lot of interesting possibilities. In later posts, I’ll talk about when we might want to create a binding between our views, as well as some interesting problems a manual binding can solve.