Skip to content

Instantly share code, notes, and snippets.

@magnuskahr
Last active May 24, 2024 23:52
Show Gist options
  • Save magnuskahr/b534d803a550cbe5dc6b65f573d5af2f to your computer and use it in GitHub Desktop.
Save magnuskahr/b534d803a550cbe5dc6b65f573d5af2f to your computer and use it in GitHub Desktop.
A simple picker to pick a enum.
import SwiftUI
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)
}
}
}
@magnuskahr
Copy link
Author

magnuskahr commented Jul 16, 2020

It is really simple to use if you have a case iterable enum with a raw value of String:

enum PickerAction: String, CaseIterable {
  case source, targets
}

Just simply:

struct ContentView: View {
    @State private var pickerAction = PickerAction.source
    var body: some View {
      EnumPicker(selected: $pickerAction)
    }
}

As it will produce a Text-view itself; but if your enum does not have such string as a raw value, you can return a view in a closure:

EnumPicker(selected: $pickerAction) { e in
  // return view here
}

and you can of course also change the picker style, just add it to the environment: (Thanks @tobiasdm)

var body: some View {
  EnumPicker(selected: $pickerAction)
    .pickerStyle(SegmentedPickerStyle())
}

PS: If you rely on a custom view instead of the string value, remember that SegmentedPickerStyle can only show one Image or one Text-view per segment.

@imthath-m
Copy link

It works great.

But in most cases, the raw values of the enums are not good enough to be displayed to the user and in some cases, we have Int as raw values. So I wrote another extension.

extension EnumPicker where T: Displayable, V == Text {
    init(selected: Binding<T>, title: String? = nil) {
        self.init(selected: selected, title: title) {
            Text($0.displayValue)
        }
    }
}

protocol Displayable {
    var displayValue: String { get }
}

@lstomberg
Copy link

lstomberg commented Feb 27, 2021

This works great.

The only change I made is to use Apple's standard CustomStringConvertable protocol instead of Displayable.

extension EnumPicker where T: CustomStringConvertible, V == Text {
    
    init(_ title: String? = nil, selected: Binding<T>) {
        self.init(title: title, selected: selected) {
            Text($0.description)
        }
    }
}

@magnuskahr
Copy link
Author

Thanks @lstomberg!

And yes, it is always great to use things from the Swift Standard Library, good think 💪🏼💪🏼

@apocolipse
Copy link

apocolipse commented Nov 14, 2021

Instead of extensions, you can use a simplified version with a default "displayer", note no need for RawRepresentable, just Hashable and CaseIterable

struct CaseIterablePicker<T: CaseIterable & Hashable> : View
  where T.AllCases: RandomAccessCollection {

  var title: String = ""
  var selection: Binding<T>
  var display: (T) -> String = { "\($0)" }

  var body: some View {
    Picker(title, selection: selection) {
      ForEach(T.allCases, id:\.self) {
        Text(display($0)).tag($0)
      }
    }
  }
}

You can pass closures to display, or KeyPath's with Swift 5.2+, which allows the following, which IMO is better as it simplifies the generic definition greatly and moves the burden on "how to display stuff" to the caller, but in a pretty easy to manage manner

// Simple case, use existing string transforms
enum TestEnum: String, CaseIterable {
  case one, two, three, four, five
}

CaseIterablePicker(selection: $testEnum, display: \.rawValue.capitalized) // Options show as "One", "Two", "Three"...
// Custom extension to String for camel case transform
enum TestEnum2: String, CaseIterable {
  case aTestCase, anotherTestCase, thirdTestCase
}

extension String {
  var camelCaseToReadable: String { ... }
}
CaseIterablePicker(selection: $testEnum2, display: \.rawValue.camelCaseToReadable.capitalizingFirstLetter)
// long chained KeyPath, options show as "A test case", "Another test case", "Third test case"
// Computed var on enum itself is just as simple and doesn't even require RawRepresentable
enum TestEnum3: CaseIterable {
  case someCase, anotherCase, whatever
  
  var localized: LocalizedStringKey { ... }
}

CaseIterablePicker(selection: $testEnum2, display: \.localized)

@magnuskahr
Copy link
Author

Hey @apocolipse!
That is also a take on using enum for pickers. If a simple text view is what always is needed, I think it is a great solution, however it misses the view flexibility of my version :-)

Also, the best of both world can be done! See this extension for my version:

extension EnumPicker where V == Text {
    init(selected: Binding<T>, title: String? = nil, display: @escaping (T) -> String) {
        self.init(selected: selected, title: title) {
            Text(display($0))
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment