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?

mfaani
  • 33,269
  • 19
  • 164
  • 293
  • Can you try it with generics? you probably need `T:` but I'm not sure which qualifier to use , i.e. `private func loadFromDB(id: ID) -> CoreDataResult{` and `T.fetchRequest()` – Alex Dec 12 '18 at 16:45

2 Answers2

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
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • can you refer me to some explanation for why you're using `static`? – mfaani Dec 12 '18 at 18:26
  • 1
    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. – vadian Dec 12 '18 at 18:29
  • 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? – mfaani Dec 12 '18 at 19:44
  • I don't know. It's just a protocol extension for the type rather than for the instance. – vadian Dec 12 '18 at 19:52
  • 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.` – mfaani Dec 12 '18 at 19:57
  • 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 :)) – mfaani Dec 18 '18 at 21:22
  • 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. – vadian Dec 18 '18 at 21:35
  • 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 share – mfaani Dec 18 '18 at 21:41
  • 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 type – vadian Dec 18 '18 at 21:44
  • To use `Codable` with Core Data please see https://stackoverflow.com/questions/44450114/how-to-use-swift-4-codable-in-core-data – vadian Dec 18 '18 at 21:54
  • Thanks. Can you also address no.2 ? That's what you had in mind? – mfaani Dec 18 '18 at 22:33
  • Yes, exactly. To call on the type as the other static methods. – vadian Dec 18 '18 at 22:34
  • 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) throws -> Self; static func insertNewObject() -> Self; }` – mfaani Dec 21 '18 at 18:44
  • No, just `Self` could be anything. The associated type constrains the type to `NSManagedObject` – vadian Dec 21 '18 at 18:51
  • Right. I feel like I sometimes just want to lessen the codes for no good reason! – mfaani Dec 21 '18 at 18:59
  • 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? – mfaani Dec 28 '18 at 19:39
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)
        }
    }
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • 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` I mean my `ManagedObject` is an `NSManagedObject` instance which clearly has nothing do with `NSFetchRequestResult` ...meaning it can't be a substitute for that. – mfaani Dec 12 '18 at 17:17
  • 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? – mfaani Dec 12 '18 at 17:18
  • @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. – Dávid Pásztor Dec 12 '18 at 17:21
  • 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. – Dávid Pásztor Dec 12 '18 at 17:22
  • followup on 2. `let fetchrequest: NSFetchRequest = TripEntity.fetchRequest()` and do `let jobs = try context.fetch(fetchrequest)` then the type of `jobs` is `[TripEntity]` ie its not `[Any]` – mfaani Dec 12 '18 at 17:24
  • 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` is because `TripEntity` is a concrete type... – mfaani Dec 14 '18 at 11:40