/// A protocol mocking some functionality of some system object.
protocol Thing: Identifiable {
var whatever: String { get }
}
extension SomeSystemObject: Thing {
var whatever: String {
// get and return whatever from system object
}
// System object conforms to Identifiable
}
class ViewModel: ObservableObject {
@Published things: [any Thing] = []
}
#if DEBUG
struct PreviewThing: Thing {
var whatever: String = ""
var id: String = ""
}
extension ViewModel {
static var forPreviews: ViewModel {
let vm = ViewModel()
vm.things = [PreviewThing(…), PreviewThing(…)]
return vm
}
}
#endif
And in the SwiftUI view body:
struct MyView: View {
@ObservedObject viewModel: ViewModel
var body: some View {
VStack {
ForEach(viewModel.things) { thing in
// do something with thing
}
}
}
}
Now, when I try to compile this, the ForEach line gives me this error: type 'any Thing' cannot conform to 'Identifiable'. Only concrete types such as structs, enums and classes can conform to protocols
I understand what it says, but… is there a better way? Should I be able to embrace Swift generics to do this?
In human language, what I try to do is: I have some system objects whose functionality I want to express with a protocol, so I can construct lightweight mocks for them for tests and SwiftUI previews. The system objects are otherwise not easily mockable (cannot be subclassed etc). I seem to be able to express what I want on the model side, but SwiftUI ForEach doesn’t like it.
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 to Thing:
class ViewModel<T: Thing>: ObservableObject {
@Published var things: [T] = []
}
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 {
…
}
}
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
…
}
}
}
}
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 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) }
}
In your view, you can now write ForEach(viewModel.things, id: \.anyHashableID).
Excellent answer! Thank you. Your assumptions were correct. There are never multiple types at the same time, and the type does not change dynamically. So, indeed, I just verbatim plugged the generics from your answer, and it now builds and works as expected.
I don’t think I can assume much about the ID. The underlying SystemObject whom I’m trying to mock conforms to Identifiable. This means that it has an id property, but as far as I know, the Identifiable contract does not say anything, or allow me assume anything, about the concrete type of the SystemObject’s id.
I’m not sure if you mean that you can’t fix ID to a type or that you shouldn’t in your use case. If you want to constraint an associated type, you can just add where ID == ConcreteType before the opening brace of the refining protocol (e.g. Thing).