Home > Enterprise >  How to get siblings of enum case to build custom picker?
How to get siblings of enum case to build custom picker?

Time:09-28

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 to Identifiable: it needs to be Hashable to work with ForEach, then force the ForEach to use \.self as the id. The raw value of the enum is of type String (you required it), using \.self will always work.
  • Fruit is not Identifiable and it doesn't need to be.
  • The main view is passing Fruit but the picker-view expects a type Sources.AllCases.Element: they don't match. Simply use Sources 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)
        }
    }
}
  • Related