SwiftUI: A picker for enums
I often create an enum
to define my finite choices of a Picker
. Say we need to pick between time units, we could have the following enum
:
enum TimeUnit: String, CaseIterable {
case second, minut, hour
}
it implements String
and CaseIterable
, so we can use it in a view like this:
struct ContentView: View {
@State private var unit: TimeUnit = .second
var body: some View {
Picker(selection: $unit, label: Text("")) {
ForEach(Array(TimeUnit.allCases), id: \.self) {
Text($0.rawValue)
}
}
.labelsHidden()
}
}
But what if we wanna make something a little smarter?
The EnumPicker
Introducing the EnumPicker
. What we will aim to make, a simple yet flexible picker that requires nothing more than giving it the parameter to change: EnumPicker(selected: $unit)
. Out basic implementation goes a follows:
struct EnumPicker<T: Hashable & RawRepresentable & CaseIterable>: View where T.RawValue == String {
@Binding var selected: T
var title = ""
var body: some View {
Picker(selection: $selected, label: Text(title)) {
ForEach(Array(T.allCases), id: \.rawValue) {
Text($0.rawValue).tag($0)
}
}
}
}
It gives us the following two initializers:
EnumPicker(selected: Binding<>)
EnumPicker(selected: Binding<>, title: String)
The picker works fine at the current state. Comparing it to our goals it is simple and somewhat flexible: We can change the title, choices defined by our enum
, and since SwiftUI works with environments, we can always change the pickerStyle
by appending .pickerStyle()
. However, we are limited in the way the enums are translated into the picker, and the enum
are required to implement String
.
We will fix this by extracting the requirement of RawRepresentable
into an extension.
struct EnumPicker<T: Hashable & CaseIterable, V: View>: View {
@Binding var selected: T
var title: String? = nil
let mapping: (T) -> V
var body: some View {
Picker(selection: $selected, label: Text(title ?? "")) {
ForEach(Array(T.allCases), id: \.self) {
mapping($0).tag($0)
}
}
}
}
extension EnumPicker where T: RawRepresentable, T.RawValue == String, V == Text {
init(selected: Binding<T>, title: String? = nil) {
self.init(selected: selected, title: title) {
Text($0.rawValue)
}
}
}
We now have two additional initializers:
EnumPicker(selected: Binding<>, mapping: (_) -> _)
EnumPicker(selected: Binding<>, title: String?, mapping: (_) -> _)
With which we can apply a mapping to how an enum
should be displayed in the Picker
.
EnumPicker(selected: $unit) { e in
Text("\(2) \(e.rawValue)s")
}
.pickerStyle(SegmentedPickerStyle())
Pickers are limited in what they can display, it can either be a Text
or an Image
.
Conclusion
We have created a simple and flexible EnumPicker
, which makes it simple to pick a case of an enum, just as long as it implements CaseIterable
. It follows standard SwiftUI design, and you will therefore see it fit right in. You can find a Gist of the final code here.