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

2

6

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?

Alvord answered 12/12, 2018 at 16:3 Comment(1)
Can you try it with generics? you probably need T:<qualifier> but I'm not sure which qualifier to use , i.e. private func loadFromDB<T:>(id: ID) -> CoreDataResult<T>{ and T.fetchRequest()Obovoid
E
8

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
    }
}
Exegetics answered 12/12, 2018 at 18:0 Comment(16)
can you refer me to some explanation for why you're using static?Alvord
As I said, rather than passing the type as parameter – which smells quite objective-c-ish – call the methods itself on the type. For example a syntax Person.loadFromDB(... is pretty efficient.Exegetics
While I fully get what you're doing. I just want to find myself some more use cases of this. But after searching variations of the following keywords protocols static func associatedtype with Self I'm not finding anything. Can you give the correct the keyword/phrase?Alvord
I don't know. It's just a protocol extension for the type rather than for the instance.Exegetics
It would be great if Vadian can make an edit, but if anyone is wondering how you can use this: then you need to 1. extend it : extension TripEntity : CoreDataWriteManagerProtocol {} 2. use it just like this TripEntity.loadFromDB(predicate: myPredicate) Long story short the type is inferred from point where you write TripEntity.Alvord
I made an edit. Can you take a look? (I will award the bounty to you...at the end of the 7 day. So answering the edit is not a requirement :))Alvord
I added a (static) method to insert a new object. As entityName is non-optional and get directly from the class description inserting an object cannot fail. To manage JSON import/export I highly recommend to adopt Codable to the affected NSManagedObject subclasses and add individual encode and decode methods. That's more code but more efficient than enumerating the attributes in a pseudo-generic way.Exegetics
1. You addressed a different question. I just wanted to avoid the casting ie avoid objective-c-ish. Anyway to not do as! ManagedObject 2. So the usage of it would be something like: let trip = Trip.insertNewObject(); trip.passengerName = "John"; trip.cost = 13; Trip.persistentContainer.viewContext.save()? 3. On adopting Codable, let me do some testing and get back to you. Though if you have a similar answer, then please shareAlvord
Casting to as! ManagedObject is perfectly fine (and necessary and safe) as insertNewObject(forEntityName:into: returns the base class NSManagedObject. It's not objective-c-ish because it considers the (generic) associated typeExegetics
To use Codable with Core Data please see #44450614Exegetics
Thanks. Can you also address no.2 ? That's what you had in mind?Alvord
Yes, exactly. To call on the type as the other static methods.Exegetics
Couldn't we just write the protocol like this: protocol CoreDataWriteManagerProtocol { static var persistentContainer : NSPersistentContainer { get }; static var entityName : String { get }; static func loadFromDB(predicate: NSPredicate?) throws -> Self; static func fetch(request: NSFetchRequest<NSFetchRequestResult>) throws -> Self; static func insertNewObject() -> Self; }Alvord
No, just Self could be anything. The associated type constrains the type to NSManagedObjectExegetics
Right. I feel like I sometimes just want to lessen the codes for no good reason!Alvord
When you learn something new, you always try to dump it everywhere :) Here I learned the ease of static functions. Now I'm trying to figure out when I should be using it. Aside from the typical guideline of where something needs to be done across all instances, two reasons could potentially push me in that direction: 1. When one of the parameters you pass is a 'type' 2. when the functions are not mutating an instance in a group manner. By mutating I mean if one function would start a trip, another would change destination and the last would add a tip. Do those guidelines make sense?Alvord
A
4

The issue is that NSManagedObject.fetchRequest() has a return type of NSFetchRequest<NSFetchRequestResult>, which is non-generic. You need to update the definition of your fetch function to account for this. Btw the function signatures of the default implementations in the protocol extension didn't actually match the function signatures in the protocol definition, so those also need to be updated.

You also need to change the implementation of fetch(request:,from:), since NSManagedObjectContext.fetch() returns a value of type [Any], so you need to cast that to [ManagedObject] to match the type signature of your own fetch method.

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<NSFetchRequestResult>, from: NSManagedObjectContext) -> CoreDataResult<ManagedObject>
    init(persistentContainer : NSPersistentContainer)
}

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

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

        return fetch(request: fetchRequest, from: persistentContainer.viewContext)
    }

    func fetch(request: NSFetchRequest<NSFetchRequestResult>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject> {
        guard let results = (try? context.fetch(request)) as? [ManagedObject] 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)
        }
    }
}
Agranulocytosis answered 12/12, 2018 at 16:49 Comment(6)
Mr robocop this compiles now. Thanks. FYI It took me a while to understand that you also made a key change here guard let results = (try? context.fetch(request)) as? [ManagedObject]. Three questions. 1. Shouldn't the compiler have thrown a warning for when I did: func fetch(request: NSFetchRequest<ManagedObject> I mean my ManagedObject is an NSManagedObject instance which clearly has nothing do with NSFetchRequestResult ...meaning it can't be a substitute for that.Alvord
2. why can't the compiler figure it out without the as? [ManagedObject]. Aren't we passing the type in the predicate by doing storableClass.fetchRequest() 3. Why do I need to wrap the try? context.fetch(request) inside a parentheses?Alvord
@Honey 1. no, since you were actually calling the method defined in the protocol extension rather than the method defined in the protocol definition. 2. because context.fetch(_:) returns [Any]. 3. otherwise the try? will be applied to the whole expression, including the casting, so the return value will be a nested Optional, which you don't want.Agranulocytosis
It seems like you have a misunderstanding caused by the fact that you are using existing methods of CoreData, none of which are generic and all of which are written in Objective-C, but expect them to be imported as generics into Swift. That's simply not possible.Agranulocytosis
followup on 2. let fetchrequest: NSFetchRequest<TripEntity> = TripEntity.fetchRequest() and do let jobs = try context.fetch(fetchrequest) then the type of jobs is [TripEntity] ie its not [Any]Alvord
to answer my last comment. Key is what David said. Basically objective-c has no understanding of generics. So when you get the value back you need to cast it from it Any type returned. You do that with as? [ManagedObject]. The reason it's able to pick the type of for let fetchrequest: NSFetchRequest<TripEntity> is because TripEntity is a concrete type...Alvord

© 2022 - 2024 — McMap. All rights reserved.