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:
-
ViewModel
receives a type parameter for a type that conforms toThing
:class ViewModel<T: Thing>: ObservableObject { @Published var things: [T] = [] }
-
ViewModel.forPreviews
is only valid whenT == PreviewThing
, so we need awhere
constraint that expresses this:extension ViewModel where T == PreviewThing { static var forPreviews: ViewModel { … } }
-
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:
-
Add a
anyHashableID
property to your protocol, including a default implementation. The purpose is to erase the concreteID
type of each conforming type toAnyHashable
:protocol Thing: Identifiable { var whatever: String { get } var anyHashableID: AnyHashable { get } } extension Thing { var anyHashableID: AnyHashable { AnyHashable(id) } }
-
In your view, you can now write
ForEach(viewModel.things, id: \.anyHashableID)
.