magnuskahr

writing code

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:

  1. EnumPicker(selected: Binding<>)
  2. 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:

  1. EnumPicker(selected: Binding<>, mapping: (_) -> _)
  2. 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.