I wouldn't say that it's a Swift limitation. When ForEach declares that it supports collections with an Identifiable element type, it intends for every element to have the same type. That can be important to ForEach's implementation or the API it wishes to present. It's not necessarily true that it will just work with heterogeneous values.
For example, the Identifiable.id property has semantic requirements - it represents the logical identity of some piece of data. It is reasonable to expect that, within a single type, we can effectively guard against id collisions - e.g. if we had a Person type, we could use Person.socialSecurityNumber to identify each person, and not have to worry about collisions.
But if we have a heterogeneous collection, the space for potential conflicts grows dramatically. Let's imagine we have a social media App where people can share contacts (as People objects), interesting movies, books, and other data -- could a Movie.id ever, by coincidence, have the same value as a Person.id or Book.id? It certainly might!
And so this is why, in this case, I think it is correct to require a manually-written wrapper type. This allows you to specify which kinds of objects can safely live together while abiding by the requirements of the Identifiable protocol. Personally, I would recommend an enum to make it clear which types live in the list together, and to make extracting the data easier:
struct ContentView: View {
enum Element: Identifiable {
case contact(Person)
case movie(Movie)
case book(Book)
var id: String {
switch self {
case .contact(let p): p.id
case .movie(let m): m.id
case .book(let b): b.id
}
}
}
var elements: [Element]
}
If they're is a risk of collisions, this also gives you the opportunity to mix additional data in to the ID to disambiguate. For example, by adding a prefix to each ID, we ensure that Person.id and Movie.id cannot collide:
var id: String {
switch self {
case .contact(let p): "p" + p.id
case .movie(let m): "m" + m.id
case .book(let b): "b" + b.id
}
}