Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift - Core Data - codegen - Make the extension conform to a protocol?

I'm finding the documentation on the new codegen feature in the Core Data Editor in Xcode 8 a bit sparse.

This is a "in Objective-C, I would...." kind of question.

I'm trying to declare a protocol that has 2 methods:

@property (strong, readonly) NSNumber *serverId;

+ (instancetype)objectWithServerId:(NSNumber*)serverId inContext:(NSManagedObjectContext*)moc;

In Objective-C, I would use mogenerator to declare that the baseclass generated should be "MyBaseClass".

and in that baseclass I can implement that class method once. In the Core Data editor, I just have to make sure my entity has that attribute. In the 'human readable' file, I would declare that it conforms to that protocol, and because it inherits from that baseclass (which is basically abstract), it can call that class method listed above.

I think with strong typing, this may not be possible. I have made it work, but each subclass I create (which uses the Xcode generated Extensions) has to implement this method, whereas I would prefer to write the method once as a generic.

In Swift, I added that attribute to the entity (no parent, therefore it is a subclass from NSManagedObject), and did this:

protocol RemoteObjectProtocol {

    var serverId: Int64 {get}

    static func object<T: NSManagedObject>(WithServerId serverId: Int64, context: NSManagedObjectContext!) -> T?
}


import CoreData


@objc(TestObject)
class TestObject: NSManagedObject {


}


extension TestObject: RemoteObjectProtocol {
    // by nature of it being in the generated core data model, we just need to declare its conformance.

    static func object<T: NSManagedObject>(WithServerId serverId: Int64, context: NSManagedObjectContext!) -> T? {

        // IF YOU FIND A BETTER WAY TO SOLVE THIS, PLEASE DO!
        // I would have loved to use a baseclass RemoteObject: NSManagedObject, where you can just implement this
        // Method.  But there was no way for it to play nicely with the CoreData Editor

        // in addition, the convenience method generated by Xcode .fetchRequest() only seems to work outside of this extension.  
        // Extensions can't make use of other extensions
        // So we create the fetch request 'by hand'

        let fetchRequest = NSFetchRequest<TestObject>(entityName: "TestObject")
        fetchRequest.predicate = NSPredicate(format: "serverId == %i", serverId)
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "serverId", ascending: true)]
        fetchRequest.fetchLimit = 1

        do {
            let fetchedObjects = try context.fetch(fetchRequest)

            return fetchedObjects.first as! T?

        } catch {
            log.warning("Failed to fetch object with serverId:\(serverId) error:\(error)")
        }

        return nil
    }
}
like image 409
horseshoe7 Avatar asked Sep 15 '25 05:09

horseshoe7


1 Answers

Codegen classes can conform to protocols. And, Swift protocols can provide a default implementation of any functions.

So, in theory at least, it should be simpler to achieve what you're doing in Swift than in Objective-C. But, getting the syntax right can be a bit tricky.

The main issues are the following:

  • Preface the protocol declaration with @objc so that it can play with CoreData and NSManagedObject
  • The function's implementation is not included in the protocol itself, but rather in an extension
  • Constrain the extension, using a where clause, to apply only to subclasses of NSManageObject (as it should). And, by doing so, the extension receives NSManageObject's functionality
  • Finally, as always, so as not to modify the Xcode codegen (that's in the Derived Data folder), conform to the protocol in an extension for each of the NSManagedObject subclasses.

So, for the protocol, the following code should do it:

import CoreData

@objc protocol RemoteObject {

    var serverId: Int64 { get }
}

extension RemoteObject where Self: NSManagedObject {

    static func objectWithServerId(_ serverId: Int64, context: NSManagedObjectContext) -> Self? {

        let fetchRequest: NSFetchRequest<Self> = NSFetchRequest(entityName: Self.entity().name!)

        let predicate = NSPredicate(format: "serverId == %d", serverId)
        let descriptor = NSSortDescriptor(key: #keyPath(RemoteObject.serverId), ascending: true)
        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = [descriptor]
        fetchRequest.fetchLimit = 1

        do {
            let results = try context.fetch(fetchRequest)
            return results.first
        } catch {
            print("Failed to fetch object with serverId:\(serverId) error:\(error)")
        }
        return nil
    }
}

And, as you noted, every entity already has the serverId attribute. So, you need only declare that it conforms to the protocol:

extension MyCoreDataEntity: RemoteObject {

}

ASIDE

Note that for some reason the compiler rejects the somewhat simpler line:

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

That simpler line generates the following error: Cannot convert value of type 'NSFetchRequest<NSFetchRequestResult>' to specified type 'NSFetchRequest<Self>'. But, for some reason, the above workaround doesn't.

Any ideas on why this occurs are very welcome.

like image 53
salo.dm Avatar answered Sep 16 '25 22:09

salo.dm