I think I'm close to understanding something important about any boxes that would improve my reasoning about them, but it's still not quite making sense. I encountered the exact situation described in this topic, but the discussion moved on to a different way to solve the problem, where I'm still not sure why the error occurs. Here's a simplified version of my code that causes the error.
import SwiftUI
protocol Thing: Identifiable {
var color: UIColor { get }
}
struct RedColoredThing: Thing {
let color: UIColor = .red
var id: UIColor { color }
}
struct BlueColoredThing: Thing {
let color: UIColor = .blue
var id: UIColor { color }
}
struct ContentView: View {
@State private var things: [any Thing] = [
RedColoredThing(),
BlueColoredThing()
]
var body: some View {
VStack {
ForEach(things) { // error: Type 'any Thing' cannot conform to 'Identifiable'
// ForEach(things, id: \.color) { // No error, works as intended.
Color(uiColor: $0.color)
}
}
}
}
(I realize that using a generic rather than existential type would be preferable, but in the case I'm addressing the concrete type isn't known at compile time. I could possibly refactor so that it is known at compile time, but I'd like to understand this issue.)
Using the alternative ForEach initializer that takes an explicit ID argument works. That seems to imply that any Thing is a not a type that conforms to Identifiable, but is a type that has all of its properties.
To eliminate any confusion on my part about whether the problem is due to Thing's adoption of Identifiable, I introduced
This results in the even more confounding (to me) error "Type 'any Thing' cannot conform to 'Thing'". My intuition about any Thing is that if there's anything () it can do, it can to conform to Thing. Instead, it seems to have all of the properties of a Thing, but not the identity of it. Is this the correct way to think about any types? Is there a technical or fundamental reason why any Thing can't conform to Thing? I appreciate any help improving my mental model of this. Thank you.
Interesting. My mental model of any Thing is that it's a built-in implementation of Hamish's suggestion "Build a type eraser", i.e., that any Thing is a concrete type that can be instantiated as an entity on which all the properties and methods of a Thing can be accessed. Is that not right?
I think part of the problem is that, in your first code sample, Thing doesn't have any constrains on its ID type, so ForEach can't handle trying to iterate through [any Thing] where there's no guarantee that the IDs are the same type. In fact I'm surprised that the commented-out line works.
I'm also surprised that using UIColor as an ID works since AFAICT it's not Hashable.
Yeah, it's perhaps odd that I used UIColor as Identifiable's ID type, but I wanted a slightly more interesting visual in the sample app while still having the simplest code possible. UIColor is not the ID type used in the actual code I wrote when I encountered this problem.
UIColor inherits NSObject which conforms to Hashable.
protocol Thing: Identifiable {
var id: UUID { get }
var color: UIColor { get }
}
or
protocol Thing: Identifiable where ID == UUID {
var id: UUID { get }
var color: UIColor { get }
}
So Swift has full expectations regarding the ID type, the code still doesn't work. So, if anyone could provide more insight on this, it would be appreciated.
In a similar code in my project I also tried the following and it worked:
protocol: Thing: Identifiable where ID == UUID {
var id: Self.ID { get }
var color: UIColor { get }
}
It compiled but I still had to pass the id parameter in the ForEach.
I'm also in the situation where the concrete type is not known at compile time, existential types seems to be the way to go, but I can't find a way to make it so that id is inferred by the compiler.
UPDATE
I was experimenting and was able to create an extension to ForEach to hide away the need to pass id at the call site:
I was under the impression that all subclasses of NSObject, and therefore (almost) all Objective-C classes, were hashable. I believe the default implementation simply casts the reference to an integer and returns that, but I could be mistaken.