I'm trying to build a SwiftUI that accepts any enum case as its selection, then it will automatically render its siblings as options. Here's what I started with, but I'm having a hard time overcoming the generics:
enum Option: String, CaseIterable, Identifiable, Equatable {
case abc
case def
case ghi
var id: Self { self }
}
struct CustomPicker<Sources>: View where Sources: RawRepresentable & CaseIterable & Identifiable, Sources.AllCases: RandomAccessCollection, Sources.RawValue == String {
@Binding var selection: Sources.AllCases.Element
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 8) {
ForEach(Sources.allCases) { item in
Text(item.rawValue)
}
}
.padding(.horizontal)
}
}
}
When I try to use it, I get a compile error: Generic parameter 'Sources' could not be inferred
:
enum Fruit: String, CaseIterable, Identifiable, Equatable {
case apple, banana, orange
}
struct ContentView: View {
@State private var selectedFruit: Fruit = .apple // Error here
var body: some View {
CustomPicker(selection: $selectedFruit) // Error here
}
}
How can I get the generics correct to be able to handle any String/RawRepresentable enum and build out its siblings automatically?
CodePudding user response:
Your code is not far from achieving the result you expect. Some comments:
- You don't need to require that
Sources
conform toIdentifiable
: it needs to beHashable
to work withForEach
, then force theForEach
to use\.self
as the id. The raw value of the enum is of typeString
(you required it), using\.self
will always work. Fruit
is notIdentifiable
and it doesn't need to be.- The main view is passing
Fruit
but the picker-view expects a typeSources.AllCases.Element
: they don't match. Simply useSources
in your@Binding
. - Remember to update the value of the
@Binding
.
Here below you have a working example:
struct ContentView: View {
@State private var selectedFruit: Fruit = .apple
@State private var selectedCar = Car.ferrari
var body: some View {
VStack {
CustomPicker(selection: $selectedFruit)
CustomPicker(selection: $selectedCar)
// Just for testing
Text("Eating one \(selectedFruit.rawValue) in a \(selectedCar.rawValue)")
.font(.largeTitle)
.padding()
}
}
}
// The Enum doesn't need to be Identifiable
enum Fruit: String, CaseIterable {
case apple, banana, orange
}
enum Car: String, CaseIterable {
case ferrari, porsche, jaguar, dacia
}
// Replace Identifiable with Hashable
struct CustomPicker<Sources>: View where Sources: RawRepresentable & CaseIterable & Hashable, Sources.AllCases: RandomAccessCollection, Sources.RawValue == String {
@Binding var selection: Sources // This is the right type
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 8) {
// Force the id as the item itself
ForEach(Sources.allCases, id: \.self) { item in
Text(item.rawValue)
// Somehow you need to update the @Binding
.onTapGesture {
selection = item
}
}
}
.padding(.horizontal)
}
}
}