Indentifiable Protocol extension for SwiftUI list data

I was experimenting with SwiftUI and came across a problem while implementing the data model for one of my List. My plan was to create a protocol CardProtocol as the data protocol for the elements of my lists and then have a CoreData implementation of the protocol as well as a dummy one for unit testing and Canvas use. If you are using a data collection in SwiftUI List the single elements need to conform to the Identifiable protocol.

The code looks like this:

import SwiftUI import Combine final class CardsModel: BindableObject { var cards: [CardProtocol] = [] let didChange = PassthroughSubject<CardsModel, Never>() } protocol CardProtocol: Identifiable { var id: Int { get set } var firstName: String? { get set } var lastName: String? { get set } var email: String? { get set } var phone: String? { get set } } 

This will not even compile as the Identifiable protocol has 2 associated types which needs to be specified if the protocol is to be used for a variable definition.

/// A type that can be compared for identity equality. public protocol Identifiable { /// A type of unique identifier that can be compared for equality. associatedtype ID : Hashable /// A unique identifier that can be compared for equality. var id: Self.ID { get } /// The type of value identified by `id`. associatedtype IdentifiedValue = Self /// The value identified by `id`. /// /// By default this returns `self`. var identifiedValue: Self.IdentifiedValue { get } } 

The exact error being error: protocol 'CardProtocol' can only be used as a generic constraint because it has Self or associated type requirements. Now ID is not an issue and can be fixed, but IdentifiedValue it's by nature different in the CoreData and the dummy implementation.

The only reasonable solution that I found was to remove compliance to Identifiable from the protocol and reintroduce it later in the View using cardsModel.cards.identified(by: \.id). Is there any better way out of this, that let me keep the Identifiable compliance at protocol level?

2

2 Answers

The only solution, beside adding Identifiable through .identified(by: \.id), is using the type erasure pattern. This uses 3 classes to box and hide the associated type and allows then to declare an array of AnyCard objects. The implementation is quite bulky and probably not worth it for my problem. But here it goes:

final class CardsModel<IdentifiedValue:CardProtocol>: BindableObject { var cards: [AnyCard<IdentifiedValue>] = [] let didChange = PassthroughSubject<CardsModel, Never>() } protocol CardProtocol: Identifiable{ var id: Int32 { get set } var firstName: String? { get set } var lastName: String? { get set } var email: String? { get set } var phone: String? { get set } } struct TestCard: CardProtocol { var id: Int32 var firstName: String? var lastName: String? var email: String? var phone: String? } extension CardsModel where IdentifiedValue == TestCard { convenience init(cards: [TestCard]) { self.init() self.cards = cards.map({ (card) -> AnyCard<TestCard> in return AnyCard(card) }) } } private class _AnyCardBase<IdentifiedValue>: CardProtocol { init() { guard type(of: self) != _AnyCardBase.self else { fatalError("_AnyCardBase<Model> instances can not be created; create a subclass instance instead") } } var id: Int32 { get { fatalError("Must override") } set { fatalError("Must override") } } var firstName: String? { get { fatalError("Must override") } set { fatalError("Must override") } } var lastName: String? { get { fatalError("Must override") } set { fatalError("Must override") } } var email: String? { get { fatalError("Must override") } set { fatalError("Must override") } } var phone: String? { get { fatalError("Must override") } set { fatalError("Must override") } } } private final class _AnyCardBox<Concrete: CardProtocol>: _AnyCardBase<Concrete.IdentifiedValue> { var concrete: Concrete init(_ concrete: Concrete) { self.concrete = concrete } override var id: Int32 { get { return concrete.id } set { concrete.id = newValue } } override var firstName: String? { get { return concrete.firstName } set { concrete.firstName = newValue } } override var lastName: String? { get { return concrete.lastName } set { concrete.lastName = newValue } } override var email: String? { get { return concrete.email } set { concrete.email = newValue } } override var phone: String? { get { return concrete.phone } set { concrete.phone = newValue } } } final class AnyCard<IdentifiedValue>: CardProtocol { private let box: _AnyCardBase<IdentifiedValue> init<Concrete: CardProtocol>(_ concrete: Concrete) where Concrete.IdentifiedValue == IdentifiedValue { box = _AnyCardBox(concrete) } var id: Int32 { get { return box.id } set { box.id = newValue } } var firstName: String? { get { return box.firstName } set { box.firstName = newValue } } var lastName: String? { get { return box.lastName } set { box.lastName = newValue } } var email: String? { get { return box.email } set { box.email = newValue } } var phone: String? { get { return box.phone } set { box.phone = newValue } } } //NSManagedObject extention extension Card:CardProtocol {} 

A simple type-eraser solves that problem.

 final class CardsModel: BindableObject { var cards: [AnyCardModel] = [AnyCardModel]() let didChange = PassthroughSubject<CardsModel, Never>() } protocol CardProtocol: Identifiable { var id: Int { get set } var firstName: String? { get set } var lastName: String? { get set } var email: String? { get set } var phone: String? { get set } } class AnyCardModel: CardProtocol { var _id: Int var _firstName: String? var _lastName: String? var _email: String? var _phone: String? init<T: CardProtocol>(card: T) { _id = card.id _firstName = card.firstName _lastName = card.lastName _email = card.email _phone = card.phone } var id: Int { get { return _id } set { _id = newValue} } var firstName: String? { get { return _firstName } set { _firstName = newValue} } var lastName: String? { get { return _lastName } set { _lastName = newValue} } var email: String?{ get { return _email } set { _email = newValue} } var phone: String?{ get { return _phone } set { _phone = newValue} } } 
2

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

You Might Also Like