Search code examples
swiftgenericsidentifiable

Identifiable as generic and func parameter


Hello I want to build something like below:

protocol BaseEntity: Identifiable {
    var id: UUID { get set }
}
protocol DataStore {
    associatedtype T: BaseEntity
    
    typealias CompleteHandler<T> = ((Result<T, Error>) -> Void)

    func read(with id: T.ID, completion: CompleteHandler<T>)
}

but in concrete implementation I want to make use of the UUID that was defined in the BaseEntity protocol:

class Storage<Entity: BaseEntity>: DataStore {
    func read(with id: Entity.ID, completion: CompleteHandler<Entity>) {
        // here, how to use that id as a UUID?
    }
}

I don't know how to do it really, to define that there must be an object conforming to BaseEntity and would like to use that UUID id property in the concrete implementation. I don't want to do it like:

func read(with id: Entity.ID, completion: CompleteHandler<Entity>) {
    guard let uuid = id as? UUID else {
        return
    }
        
    // more code goes here...
}

How I should do it?


Solution

  • Well, basically the lack of any concrete type in this implementation is what troubles the compiler to figure out the Entity.ID type.

    This can be shown by not using a generic Entity type in your Storage implementation, like this:

    struct MyEntity: BaseEntity {
        var id: UUID
    }
    
    class Storage: DataStore {
        func read(with id: MyEntity.ID, completion: CompleteHandler<MyEntity>) {
            print(id.uuidString) // <- `id` is of `UUID` type
        }
    }
    

    Solution 1: To help the compiler understand the Entity.ID type, you can conditionally conform to the DataStore protocol, by stating that Entity.ID will always be of UUID type (which is actually what you want in your implementation):

    class Storage<Entity: BaseEntity>: DataStore where Entity.ID == UUID {
        func read(with id: Entity.ID, completion: CompleteHandler<Entity>) {
            print(id.uuidString) // again this line passes compilation
        }
    }
    

    Solution 2: Even better, you can conditional constraint your BaseEntity protocol by stating that ID will always be of UUID type. That way you do not need to change your Storage implementation:

    protocol BaseEntity: Identifiable where ID == UUID {}
    // or using the following which is also the same
    protocol BaseEntity: Identifiable<UUID> {}
    
    class Storage<Entity: BaseEntity>: DataStore {
        func read(with id: Entity.ID, completion: CompleteHandler<Entity>) {
            print(id.uuidString) // again this line passes compilation
        }
    }
    

    Solution 3: Another solution, which might be more appropriate in your context, is to convert BaseEntity to a class. That way you also do not need to change your Storage implementation:

    class BaseEntity: Identifiable {
        var id: UUID
        
        init(id: UUID) {
            self.id = id
        }
    }
    
    class Storage<Entity: BaseEntity>: DataStore {
        func read(with id: Entity.ID, completion: CompleteHandler<Entity>) {
            print(id.uuidString) // again this line passes compilation
        }
    }