-
-
Save magnuskahr/b534d803a550cbe5dc6b65f573d5af2f to your computer and use it in GitHub Desktop.
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) | |
} | |
} | |
} |
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 }
}
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)
}
}
}
Thanks @lstomberg!
And yes, it is always great to use things from the Swift Standard Library, good think 💪🏼💪🏼
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)
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))
}
}
}
It is really simple to use if you have a case iterable enum with a raw value of String:
Just simply:
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:and you can of course also change the picker style, just add it to the environment: (Thanks @tobiasdm)
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.