How to iterate over an array with protocol-type elements in SwiftUI?

This sounds like you want the ViewModel.things array to be able to hold values of different types, but never multiple types at the same time, and the type of the elements doesn't suddenly change dynamically.

If true, this suggests that [any Thing] is indeed the wrong abstraction because you don't need the type-erasing nature of an any type (existential). With generics, your code would look like this:

  1. ViewModel receives a type parameter for a type that conforms to Thing:

    class ViewModel<T: Thing>: ObservableObject {
        @Published var things: [T] = []
    }
    
  2. ViewModel.forPreviews is only valid when T == PreviewThing, so we need a where constraint that expresses this:

    extension ViewModel where T == PreviewThing {
        static var forPreviews: ViewModel {
            …
        }
    }
    
  3. The view must also become generic:

    struct MyView<T: Thing>: View {
        @ObservedObject var viewModel: ViewModel<T>
    
        var body: some View {
            VStack {
                // This works now
                ForEach(viewModel.things) { thing in
                    …
                }
            }
        }
    }
    

@Ben_Cohen wrote a great answer to a related question a few weeks ago. The domain of that question is a little different, but the fundamental reasoning is exactly the same: Do `any` and `some` help with "Protocol Oriented Testing" at all? - #4 by Ben_Cohen


Sidenote: I do believe that generics are the way to go in your case. But if you really need to preserve [any Thing] as your array type (e.g. because you want to store different types at the same time), here's a workaround to make your original code work:

  1. Add a anyHashableID property to your protocol, including a default implementation. The purpose is to erase the concrete ID type of each conforming type to AnyHashable:

    protocol Thing: Identifiable {
        var whatever: String { get }
    
        var anyHashableID: AnyHashable { get }
    }
    
    extension Thing {
        var anyHashableID: AnyHashable { AnyHashable(id) }
    }
    
  2. In your view, you can now write ForEach(viewModel.things, id: \.anyHashableID).

7 Likes