Reputation: 331
In a ViewModel I have:
public var models: [any Tabbable]
And Tabbable starts with
public protocol Tabbable: Identifiable {
associatedtype Id
var id: Id { get }
/// ...
}
In Swift in my ViewModel I can use the Array.forEach to:
models.forEach { _ in
print("Test")
}
But in SwiftUI I can't use ForEach to:
ForEach(viewModel.models) { _ in
Text("Test")
}
Due to:
Type 'any Tabbable' cannot conform to 'Identifiable'
Any suggestions? This is with Swift 5.7. Do I need to go back to making something like AnyTabbable? Thanks in advance.
Upvotes: 7
Views: 3293
Reputation: 904
Scott's answer explains very well the reason why the compiler throws that error. I was experimenting with a way to give the compiler the necessary information to make this code compile and found a solution, but it requires adding constraints to your protocol and extending ForEach
.
you need to constraint the ID
type of your protocol to a concrete Hashable
type, like UUID
. It has very specific syntax. This can be a downside if your use case depends on different types for ID
.
protocol Tabbable: Identifiable where ID == UUID {
var id: Self.ID { get }
}
Then you need to extend ForEach and give it an initializer that have all the necessary information to infer the id parameter for you, again, it has very specific syntax:
extension ForEach where ID == UUID, Content: View, Data.Element == any Tabbable {
init(_ data: Data, @ViewBuilder content: @escaping (any Tabbable) -> Content) {
self.init(data, id: \.id, content: content)
}
}
Then you will be able to pass a value of type [any Tabbable]
to a ForEach
as expected. I don't fully understand why the extension is needed, but without it, the code would not compile unless you pass id
explicitly when initializing ForEach
.
Upvotes: 2
Reputation: 23701
Consider this example in the form of a Playground:
import UIKit
import SwiftUI
public protocol Tabbable: Identifiable {
associatedtype Id
var id: Id { get }
var name : String { get }
}
struct TabbableA : Tabbable{
typealias Id = Int
var id : Id = 3
var name = "TabbableA"
}
struct TabbableB : Tabbable {
typealias Id = UUID
var id : Id = UUID()
var name = "TabbableB"
}
struct ViewModel {
public var models: [any Tabbable]
}
let tabbableA = TabbableA()
let tabbableB = TabbableB()
let models: [any Tabbable] = [tabbableA, tabbableB]
struct ContentView {
@State var viewModel : ViewModel = ViewModel(models: models)
var body : some View {
ForEach(viewModel.models) { model in
Text(model.name)
}
}
}
In this case, we have the type TabbableA
where each instance has an id
property that is an integer (for the sake of the sample they only use "3" but it's the type that is significant). In TabbableB
each instance's id
is a UUID. Then I create an array where one item is an instance of TabableA
and another is an instance of TabbableB
. The array is of type [any Tabbable]
.
Then I try to use ForEach
on the array. But some elements in the array use Int
ids and some use UUID
ids. The system doesn't know what type to use to uniquely identify views. Ints
and UUIDs
can't be directly compared to one another to determine equality. While each item that went into the array is Tabbable
, and therefore conforms to Identifiable
, the elements coming out of the array, each of which is of type any Tabbable
, do not conform to Identifiable
. So the system rejects my code.
Another way to think about this: A value whose type is any SomeProtocol
is a box that can contain anything that conforms to SomeProtocol
, but the box itself does not conform to SomeProtocol
Upvotes: 10
Reputation: 499
You can provide a KeyPath to the id (or any unique variable): parameter which specifies how to retrieve the ID
in ForEach
. It is because all items in ForEach
must be unique. You can create a structure, which conforms to your protocol. At that moment it should work. The second option is to remove Identifiable
from the protocol and then you can use that directly.
public protocol Tabbable {
var id: String { get }
/// ...
}
public var models: [Tabbable]
ForEach(viewModel.models, id: \.id) { _ in
Text("Test")
}
or
public protocol TabbableType: Identifiable {
associatedtype Id
var id: Id { get }
/// ...
}
struct Tabbable: TabbableType {
var id: String { get }
/// ...
}
public var models: [Tabbable]
ForEach(viewModel.models) { _ in
Text("Test")
}
Upvotes: 1