Compile error: Generic struct 'ForEach' requires that 'S.AllCases' conform to 'RandomAccessCollection': but .allCases is fine when using it directly

Hope my sample code explain the problem I'm facing: I want to make a SwiftUI.Picker view that choose value of a enum type conforming to Hashable, CaseIteratable. Why this compile error? How to fix?

import SwiftUI

// Abstract type of value selectable via SwiftUI.Picker
protocol PickerEnum: Hashable, CaseIterable {
    var displayName: String { get }
}

// A generic Picker that choose value of type PickerEnum
struct MyPicker<S: PickerEnum, Label: View>: View {
    var selection: Binding<S>
    let label: Label

    var body: some View {
        Picker(selection: selection, label: label) {
            // Problem here: compile error
            ForEach(S.allCases, id: \.self) {    // Error: Generic struct 'ForEach' requires that 'S.AllCases' conform to 'RandomAccessCollection'
                Text($0.displayName).tag($0)
            }
        }
        .pickerStyle(SegmentedPickerStyle())
    }
}


struct PickerDemo: View {
    // Have many of these enum types choosable via SwiftUI.Picker
    enum ColorMode: PickerEnum {
        case light, dark, invisible

        var displayName: String {
            String(describing: self).capitalized
        }
    }

    enum InputMode: PickerEnum {
        case grid, spectrum, sliders

        var displayName: String {
            String(describing: self).capitalized
        }
    }

    @State private var colorMode = ColorMode.light
    @State private var inputMode = InputMode.grid

    var body: some View {
        VStack {
            Text("Color: \(colorMode.displayName), Input type: \(inputMode.displayName)")

            //
            // Instead of have to write out Picker.init each time
            //
            Picker(selection: $colorMode, label: Text("Mode")) {
                ForEach(ColorMode.allCases, id: \.self) {           // <== ForEach(E.allCases is fine here!!
                    Text($0.displayName).tag($0)
                }
            }
            .pickerStyle(SegmentedPickerStyle())

            //
            // Simply do this instead:
            //
            MyPicker(selection: $colorMode, label: Text("Mode"))

            // Again, same
            Picker(selection: $inputMode, label: Text("Input type")) {
                ForEach(InputMode.allCases, id: \.self) {
                    Text($0.displayName).tag($0)
                }
            }
            .pickerStyle(SegmentedPickerStyle())

            MyPicker(selection: $inputMode, label: Text("Input type"))
        }
    }
}

struct PickerDemo_Previews: PreviewProvider {
    static var previews: some View {
        PickerDemo()
    }
}
1 Like

I solved my problem with:

            ForEach(Array(S.allCases), id: \.self) {    // Error: Generic struct 'ForEach' requires that 'S.AllCases' conform to 'RandomAccessCollection'

still wonder why the need to do this here, but not when using it directly. Will this maintain the ordering?

2 Likes

ForEach requires that the thing is a RandomAccessCollection
CaseIterable doesn't require that AllCases is a RandomAccessCollection
That's why you're seeing the error you see.

however
Default implementation of CaseIterable happens to be an Array, which is a RandomAccessCollection

Possible solutions:

  • Require that AllCases of PickerEnum is an array
protocol PickerEnum: Hashable, CaseIterable where AllCases == Array<Self> {
    var displayName: String { get }
}
  • Require that AllCases of S is an array
struct MyPicker<S: PickerEnum, Label: View>: View where S.AllCases == Array<S> {
  • use the enums directly, so that compiler knows that AllCases is an array in this specific case (you stumbled upon this, but it's kinda counterproductive)
ForEach(ColorMode.allCases, id: \.self) { 
3 Likes

For things like this, check the type constraints. With the form you're invoking, you're trying to call .init(_:id:content:). The function itself doesn't add any additional contract on the type. Since it uses ForEach's generic parameter, you need to follow the ForEach's contract. From the declaration,

struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable

ForEach has two contracts

  • Data conforms to RandomAccessCollection, and
  • ID conforms to Hashable.

So when you call,

ForEach(S.allCases, id: \.self) { ... }

the compiler can infer that ForEach.Data is S.AllCases and ForEach.ID is S, and you need to make sure that S.AllCases conforms to RandomAccessCollection and S is Hashable.

Now, S is a generic, so what do we know?

  • S conforms to PickerEnum (from generic constraint),
  • S conforms to CaseIterable (from PickerEnum conformance),
  • S conforms to Hashable (from PickerEnum conformance), and
  • S.allCases conforms to Collection, with element being S (from CaseIterableConformance).

You see that there's simply not enough information to proof S.AllCases conforms to RandomAccessCollection. And Array(S.allCases) works because now ForEach.Data is Array<S.Element>, which meets the criteria. If you're sure that any type you're using only use RandomAccessCollection for AllCases, you can just add it to the constraint:

protocol PickerEnum: Hashable, CaseIterable where AllCases: RandomAccessCollection {
    var displayName: String { get }
}

PS

I'd suggest that you try to reduce the problem before doing some code dump. You'll see that you don't need 80+ lines of code to illustrate the problem. The first 20 is already enough. Also, with small-enough question, it's more likely to get replied (nobody wants to read an encyclopaedia/code dump to answer a question). Better yet, you may notice what's wrong and fix it right away without wasting a forum round trip.

2 Likes

I know what you mean. Will try to give more thought to minimize my samples. Hopefully, as I understand more of the problem, I can do better.

You guys are chefs, I'm trying to be a fry cook. I learn a lot from y'all!

:star_struck::pray:

I got stuck on a different "My Vanquished by Type Inference" problem:

So I don't like hardcoding .pickerStyle(SegmentedPickerStyle()), but moving this out doesn't seem to override the default:

MyPicker(selection: $colorMode, label: Text("Mode"))
    .pickerStyle(SegmentedPickerStyle())    // doesn't work

So I want to rid of MyPicker type, just extend Picker to add a new .init():

extension Picker where SelectionValue: PickerEnum {
    init(selection: Binding<SelectionValue>, label: Label) {
        self.init(selection: selection, label: label) {
            ForEach(SelectionValue.allCases, id: \.self) {
                Text($0.displayName).tag($0)
            } as! Content       // add this to make it compile
        }
    }
}

This compile, but error at the call site:

Picker(selection: $colorMode, label: Text("Mode")) // Error: Generic parameter 'Content' could not be inferred.

So to help infere Content, but doesn't compile:

extension Picker where SelectionValue: PickerEnum {
    init(selection: Binding<SelectionValue>, label: Label, content: Content.Type = ForEach<Data, ID, Content>.self) { // Error: Cannot find type 'ID' in scope
...
}
My code:
import SwiftUI

// Values selectable via SwiftUI.Picker
protocol PickerEnum: Hashable, CaseIterable where AllCases: RandomAccessCollection {
    var displayName: String { get }
}

extension PickerEnum {
    var displayName: String {
        String(describing: self).capitalized
    }
}

// A generic Picker that choose value of type PickerEnum
struct MyPicker<S: PickerEnum, Label: View>: View {
    var selection: Binding<S>
    let label: Label

    var body: some View {
        Picker(selection: selection, label: label) {
            // Problem here: compile error
            ForEach(S.allCases, id: \.self) {    // Error: Generic struct 'ForEach' requires that 'S.AllCases' conform to 'RandomAccessCollection'
                Text($0.displayName).tag($0)
            }
        }
        .pickerStyle(SegmentedPickerStyle())
    }
}

extension Picker where SelectionValue: PickerEnum {
    init(selection: Binding<SelectionValue>, label: Label, content: Content.Type = ForEach<Data, ID, Content>.self) { // Error: Cannot find type 'ID' in scope
        self.init(selection: selection, label: label) {
            ForEach(SelectionValue.allCases, id: \.self) {
                Text($0.displayName).tag($0)
            } as! Content
        }
    }
}

struct PickerDemo: View {
    // Have many of these enum types choosable via SwiftUI.Picker
    enum ColorMode: PickerEnum {
        case light, dark, invisible
    }

    enum InputMode: PickerEnum {
        case grid, spectrum, sliders
    }

    @State private var colorMode = ColorMode.light
    @State private var inputMode = InputMode.grid

    var body: some View {
        VStack {
            Text("Color: \(colorMode.displayName), Input type: \(inputMode.displayName)")

            Picker(selection: $colorMode, label: Text("Mode"))          // Error: Generic parameter 'Content' could not be inferred
            Picker(selection: $inputMode, label: Text("Input type"))    // Error: Generic parameter 'Content' could not be inferred
        }
    }
}

struct PickerDemo_Previews: PreviewProvider {
    static var previews: some View {
        PickerDemo()
    }
}

So I find out the type of:

            ForEach(SelectionValue.allCases, id: \.self) {
                Text($0.displayName).tag($0)
            } as! Content       // add this to make it compile

is something like this:

ForEach<Array<PickerEnum>, SelectionValue, ModifiedContent<Text, _TraitWritingModifier<TagValueTraitKey<PickerEnum>>>>

I was hoping I can use this and .self as the Content.Type, but TagValueTraitKey is not found, something internal to SwiftUI maybe.

Let's step back a bit and see why this won't work. The function you just made looks like this:

extension Picker where SelectionValue: PickerEnum {
  init(selection: Binding<SelectionValue>, label: Label)
}

Now there are still three generic parameters, Data, ID, and Content. So caller needs to specify them, either by type inference or explicit annotation.

Since you're not constraining any of the generic parameters, you're telling the caller that they can use any type they want. Which includes calling it like this:

Picker<[Int], Int, Text>(selection: ..., label: ...)

You can see that this most definitely won't be the view you want. This is caused by the fact that your constraint is too lenient. Before laying down the API, let's see, we want a function that:

  • Returns a Picker<Label, SelectionValue, Content>,
  • Caller decides the type of SelectionValue,
  • Caller decides the type of Label,
  • Callee decides the type of Content,
    • Specifically, Content is
      ForEach<
        SelectionValue.AllCases,
        SelectionValue,
        ModifiedContent<Text, _TraitWritingModifier<TagValueTraitKey<SelectionValue>>>>
      

We can start as a global function, and see where this would fit. Since SelectionValue and Label is specified by the caller, we can use generic parameter:

func makePicker<SelectionValue, Label>(selection: Binding<SelectionValue>, label: Label) -> ??

Now, what can we use for ??? Not a lot, actually. Remember, we want to return a picker but want to conceal Content, so we need to hide it. some is a good tool for this

func makePicker<SelectionValue, Label>(selection: Binding<SelectionValue>, label: Label) -> some View

Since in all honesty, you probably won't care that it's a Picker, and not just some View.

So now we get the function outline, it has two type parameters SelectionValue and Label, which is selected by the caller, and returns some view whose type the caller doesn't care (other than the fact that it conforms to View). Perfect, just what we want.

Then we can put in the implementation:

func makePicker<SelectionValue, Label>(selection: Binding<SelectionValue>, label: Label) -> some View {
  Picker(selection: selection, label: label) {
    ForEach(SelectionValue.allCases, id: \.self) {
      Text($0.displayName).tag($0)
    }
  }
}

Now there no weird as! anymore, since the type makes sense with the function signature.

The question is, can we put it somewhere else? Maybe in Picker? The answer is likely no. Remember, you need to know the full type to call a function. Even if we make makePicker a static member of Picker. It's still

Picker<Label, SelectionValue, Content>.makePicker(...) -> some View

Now it doesn't looks like it has anything to do with Picker. Worse, we're now back to the problem with specifying Content.

We could gather functions like this in one place, like Publishers did (notice the s).

All in all, we could make a global function, or put it in a namespace. You can also add constraint to that Picker.init of yours:

extension Picker where SelectionValue: PickerEnum, Content == ...

but I think that's a little too much burden on the caller side, to know the type of this Content.

Anyhow, I'd suggest that you investigate why you simply can't move pickerStyle outside.

It works just find on Swift Playground.

protocol PickerEnum: Hashable, CaseIterable where AllCases: RandomAccessCollection {
  var displayName: String { get }
}

// A generic Picker that choose value of type PickerEnum
struct MyPicker<S: PickerEnum, Label: View>: View {
  var selection: Binding<S>
  let label: Label

  var body: some View {
    Picker(selection: selection, label: label) {
      ForEach(S.allCases, id: \.self) {
        Text($0.displayName).tag($0)
      }
    }
  }
}
enum InputMode: PickerEnum {
  case grid, spectrum, sliders

  var displayName: String {
    String(describing: self).capitalized
  }
}

PlaygroundPage.current.setLiveView(
  MyPicker (selection: .constant(InputMode.grid), label: Text("Test"))
    .pickerStyle(SegmentedPickerStyle()))

Good thing you follow up. There's nothing more dangerous than (you're) confused and (it) compiles. Adding things to please ye almighty compiler definitely fits the bill. It's a recipe for disaster essentially everywhere.

1 Like

:pray:

investigate why you simply can't move pickerStyle outside.

In Xcode preview this doesn't work. Running in simulator and device works.

Maybe you could file a bug, with the apple's feedback assistant.

1 Like