How do you create a SwiftUI picker that works with optional selection so that not picking a value is possible?
SwiftUI Picker
A SwiftUI picker expects a binding to a selection. For example, suppose I have a project model that is Identifiable
:
struct Project: Identifiable {
let id = UUID()
var name: String
...
}
To pick a single project from a store of projects:
struct ProjectPicker: View {
@Binding var project: Project.ID
@Environment(ProjectStore.self) private var store
var body: some View {
Picker(selection: $project) {
ForEach(store.projects) { project in
Text(project.name)
}
} label: {
Text("Project")
}
}
}
The ForEach
automatically applies a tag to each Text view using the id
of the project. Selecting an item in the picker sets the pickers binding to the identifier of the item. Here’s how that looks when part of a Form
to create an item in a project:
Optional Selection
Sometimes I need a picker to work with an optional binding. Either because I don’t yet have a selection or because not selecting an item is also a valid choice:
struct ProjectPicker: View {
@Binding var project: Project.ID?
...
}
Apple provides an example on how to make this work in the documentation for the tag modifier. We need to manually tag each of the picker views with an optional identifier matching the optional type of the picker selection. The tag
modifier has a parameter to indicate you want to make it optional:
Text(project.name)
.tag(project.id, includeOptional: true)
Since the includeOptional
parameter defaults to true
it’s enough to tag the views:
Text(project.name)
.tag(project.id)
Finally, we can also include a None
option for when no project is selected. Note we need to cast nil
as an optional project ID:
Text("None")
.tag(nil as Project.ID?)
The full project picker with optional selection:
struct ProjectPicker: View {
@Binding var project: Project.ID?
@Environment(ProjectStore.self) private var store
var body: some View {
Picker(selection: $project) {
Text("None")
.tag(nil as Project.ID?)
ForEach(store.projects) { project in
Text(project.name)
.tag(project.id)
}
} label: {
Text("Project")
}
}
}