Search code examples
swiftgenericscore-dataprotocolsassociated-types

Swift can't infer generic type when generic type is being passed through a parameter


I'm writing a generic wrapper class for core data.

Here are some of my basic types. Nothing special.

typealias CoreDataSuccessLoad = (_: NSManagedObject) -> Void
typealias CoreDataFailureLoad = (_: CoreDataResponseError?) -> Void
typealias ID = String


enum CoreDataResult<Value> {
    case success(Value)
    case failure(Error)
}

enum CoreDataResponseError : Error {
    typealias Minute = Int
    typealias Key = String
    case idDoesNotExist
    case keyDoesNotExist(key: Key)
    case fetch(entityName: String)
}

I've abstracted my coredata writes in a protocol. I'd appreciate if you let me know of your comments about the abstraction I'm trying to pull off. Yet in the extension I run into the following error:

Cannot convert value of type 'NSFetchRequest' to expected argument type 'NSFetchRequest<_>'

Not sure exactly how I can fix it. I've tried variations of changing my code but didn't find success...

protocol CoreDataWriteManagerProtocol {
    associatedtype ManagedObject : NSManagedObject

    var persistentContainer : NSPersistentContainer {get}
    var idName : String {get}
    func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
    func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
    func fetch(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext)
    init(persistentContainer : NSPersistentContainer)
}

extension CoreDataWriteManagerProtocol {
    private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
        let predicate = NSPredicate(format: "%@ == %@", idName, id)

        let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
        fetchRequest.predicate = predicate

        // ERROR at below line!
        return fetch(request: fetchRequest, from: persistentContainer.viewContext) 
    }

    func fetch<ManagedObject: NSManagedObject>(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject>{
        guard let results = try? context.fetch(request) else {
            return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // @TODO not sure if entityName gets passed or not.
        }
        if let result = results.first {
            return .success(result)
        }else{
            return .failure(CoreDataResponseError.idDoesNotExist)
        }
    }
}

Additionally if I change the line:

let fetchRequest : NSFetchRequest = storableClass.fetchRequest()

to:

let fetchRequest : NSFetchRequest<storableClass> = storableClass.fetchRequest()

I get the following error:

Use of undeclared type 'storableClass'`

My intuition tells me that the compiler can't map 'parameters that are types' ie it doesn't understand that storableClass is actually a type. Instead it can only map generics parameters or actual types. Hence this doesn't work.

EDIT:

I used static approach Vadian and wrote this:

private func create(_ entityName: String, json : [String : Any]) throws -> ManagedObject {

    guard let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: Self.persistentContainer.viewContext) else {
        print("entityName: \(entityName) doesn't exist!")
        throw CoreDataError.entityNotDeclared(name: entityName)
    }

    let _ = entityDescription.relationships(forDestination: NSEntityDescription.entity(forEntityName: "CountryEntity", in: Self.persistentContainer.viewContext)!)
    let relationshipsByName = entityDescription.relationshipsByName

    let propertiesByName = entityDescription.propertiesByName

    guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
        throw CoreDataError.entityNotDeclared(name: entityName)
    }

    for (propertyName,_) in propertiesByName {
        if let value = json[propertyName] {
            managedObj.setValue(value, forKey: propertyName)
        }
    }
    // set all the relationships
    guard !relationshipsByName.isEmpty else {
        return managedObj
    }

    for (relationshipName, _ ) in relationshipsByName {
        if let object = json[relationshipName], let objectDict = object as? [String : Any] {
            let entity = try create(relationshipName, json: objectDict)
            managedObj.setValue(entity, forKey: relationshipName)
        }
    }
    return managedObj
}

But the following piece of it is not generic as in I'm casting it with as? ManagedObject. Basically it's not Swifty as Vadian puts it:

guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
    throw CoreDataError.entityNotDeclared(name: entityName)
}

Is there any way around that?


Solution

  • My suggestion is a bit different. It uses static methods

    Call loadFromDB and fetch on the NSManagedObject subclass. The benefit is that always the associated type is returned without any further type cast.

    Another change is throwing errors. As the Core Data API relies widely on throwing errors my suggestion is to drop CoreDataResult<Value>. All errors are passed through. On success the object is returned, on failure an error is thrown.

    I left out the id related code and the update method. You can add a static func predicate(for id : ID)

    protocol CoreDataWriteManagerProtocol {
        associatedtype ManagedObject : NSManagedObject = Self
    
        static var persistentContainer : NSPersistentContainer { get }
        static var entityName : String { get }
        static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject
        static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject
        static func insertNewObject() -> ManagedObject
    }
    
    extension CoreDataWriteManagerProtocol where Self : NSManagedObject {
    
        static var persistentContainer : NSPersistentContainer {
            return (UIApplication.delegate as! AppDelegate).persistentContainer
        }
    
        static var entityName : String {
            return String(describing:self)
        }
    
        static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject {
            let request = NSFetchRequest<ManagedObject>(entityName: entityName)
            request.predicate = predicate
            return try fetch(request: request)
        }
    
        static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject {
            guard let results = try? persistentContainer.viewContext.fetch(request) else {
                throw CoreDataResponseError.fetch(entityName: entityName)
            }
            if let result = results.first {
                return result
            } else {
                throw CoreDataResponseError.idDoesNotExist
            }
        }
    
        static func insertNewObject() -> ManagedObject {
            return NSEntityDescription.insertNewObject(forEntityName: entityName, into: persistentContainer.viewContext) as! ManagedObject
        }
    }