Home > Software design >  Using SwiftUI ForEach to iterate over [any Protocol] where said protocol is Identifiable
Using SwiftUI ForEach to iterate over [any Protocol] where said protocol is Identifiable

Time:06-28

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.

CodePudding user response:

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.

CodePudding user response:

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")
    }
  • Related