Define Array of protocol which conforms to Identifiable

I've got Yet Another Protocol Question. Sorry, folks.


The Swift 5.1 manual describes creating a `Collection` where the `Element` is a simple protocol, but doesn't explain how to make a Collection where the Element is a protocol with an associatedtype. In my case it's a protocol `Playable` that requires conformance to `Identifiable`.


I am fine with requiring the `Identifiable.ID` type to be `Int` for all types which adopt `Playable`, but I don't know how to express that qualification/requirement. The bits hit the fan when I try to declare something to be `[Playable]`.


My playground code might make my question clear.


protocol Playable: Identifiable
// DOESN'T WORK    where ID: Int
{
// DOESN'T HELP    associatedtype ID = Int
// DOESN'T HELP    var id: Int { get }
    func play()
}
struct Music: Playable {
    var id: Int
    func play() { print("playing music #\(id)") }
}
(Music(id: 1)).play() // displays: "playing music #1\n"

class Game: Playable {
    var id: Int
    
    init(id: Int) {
        self.id = id
    }

    func play() { print("playing game #\(id)") }
}
(Game(id: 2)).play() // displays: "playing game #2\n"

enum Lottery: Int, Playable {
    case state = 100
    case church
    case charity

    var id: Int {
        return self.rawValue
    }

    func play() { print("playing lottery \(self) #\(self.rawValue)") }
}
Lottery.church.play() // displays: "playing lottery church #101\n"

var stuff: [Playable] = [] // error: Protocol 'Playable' can only be used as a generic constraint because it has Self or associated type requirements

stuff.append(Music(id: 10))
stuff.append(Game(id: 24))
stuff.append(Lottery.charity)
stuff.map { $0.id }

My goal is to have different structs and classes conforming to Playable (and hence to Identifiable) able to live inside a Collection.

Post not yet marked as solved Up vote post of LogicalLight Down vote post of LogicalLight
6.6k views

Replies

My goal is to have different structs and classes conforming to Playable (and hence to Identifiable) able to live inside a Collection.


Impossible. As the error message is clearly stating, you cannot use protocols with associated type for an Element type of an Array or any other Collection types. And Identifiable definitely defined with an associated type, thus you cannot have such Collection.


You need to find a workaround or completely re-structure your strategy. Why do you want to make such a Collection?

I am using a List control in SwiftUI which requires Identifiable. The data source will be an Array of type X where X has two implementations depending on how data will be generated or stored. It would be ugly to have one implementation that has to have mixed responsibilities.

Whether you think something ugly or not, impossible is impossible.


Just for working with List of SwiftUI, you know that you can create an Array of concrete type which conforms to Identifiable.


An example:

protocol Playable {
    var id: Int {get}
    func play()
}
struct Music: Playable {
    var id: Int
    func play() { print("playing music #\(id)") }
}
  
class Game: Playable {
    var id: Int
      
    init(id: Int) {
        self.id = id
    }
  
    func play() { print("playing game #\(id)") }
}
  
enum Lottery: Int, Playable {
    case state = 100
    case church
    case charity
  
    var id: Int {
        return self.rawValue
    }
  
    func play() { print("playing lottery \(self) #\(self.rawValue)") }
}

//Create a concrete type conforming to Identifiable.
struct PlayableContainer: Identifiable {
    var content: Playable

    var id: Int {
        content.id
    }
    
    func play() {
        content.play()
    }
}

//For convenience...
extension PlayableContainer {
    static func music(id: Int) -> PlayableContainer {
        return PlayableContainer(content: Music(id: id))
    }
    
    static func game(id: Int) -> PlayableContainer {
        return PlayableContainer(content: Game(id: id))
    }
    
    static func lottery(_ lot: Lottery) -> PlayableContainer {
        return PlayableContainer(content: lot)
    }
}

var stuff: [PlayableContainer] = []

stuff.append(.music(id: 10))
stuff.append(.game(id: 24))
stuff.append(.lottery(.charity))
stuff.map { $0.id }

I never doubted it was impossible.


Using a proxy is much less ugly an alternative so I'll go with that.

If the id type is the same, you can use "Type Erasure", but as the name implies, you will be dealing with a reduced type, which is probably not what you want.

Code Block
class AnyIdentifiable<T>: Identifiable {
    var id: T { _id }
    private let _id: T
    init<U: Identifiable>(_ identifiable: U) where U.ID == T {
        _id = identifiable.id
    }
}
struct TypeA: Identifiable {
    var id = UUID()
}
struct TypeB: Identifiable {
    var id = UUID()
var name = "b"
}
let a = TypeA()
let b = TypeB()
let anyA  = AnyIdentifiable(a)
/** - Note: `anyB` does not have a `name` property as it is of type AnyIdentifiable*/
let anyB = AnyIdentifiable(b)
let anyIDs: [AnyIdentifiable<UUID>] = [anyA, anyB]

You can apply similar logic above creating an AnyPlayable which will also need to forward your play() but again, you will be dealing with a reduced type. So only the properties in AnyPlayable will be available, ie. id and play().


Add a Comment

This functionality has been implemented in Swift 5.7.

For the use case of SwiftUI ForEach, you can work around the limitation by using explicit id param.

ForEach(problematicArray, id: \.id) { entry in ... }

Edit: Or perhaps not. It no longer errors in Xcode (14.1), but the compiler dies when attempting to build. Oops.