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
Sourcesconform toIdentifiable: it needs to beHashableto work withForEach, then force theForEachto use\.selfas the id. The raw value of the enum is of typeString(you required it), using\.selfwill always work. Fruitis notIdentifiableand it doesn't need to be.- The main view is passing
Fruitbut the picker-view expects a typeSources.AllCases.Element: they don't match. Simply useSourcesin 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)
}
}
}
