4

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
    }
}
horseshoe7
  • 2,745
  • 2
  • 35
  • 49

1 Answers1

3

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.

salo.dm
  • 2,317
  • 1
  • 15
  • 16
  • This is beautiful. Thank you! I learned something today. The reason why I think that doesn't occur is because the .fetchRequest() method is defined in a class extension (created by Xcode's codegen), and so these two extensions aren't 'aware' of each other?? I'm not sure however as to the theoretical reason for that. In Objective-C you know which category gets imported before which, so I don't know about Swift. – horseshoe7 Dec 13 '16 at 12:20
  • @horseshoe7, You may be onto something, though I think `entity()` is defined in the same place. It seems the compiler doesn't type-check the let statement when it receives the fetch request from `NSFetchRequest(entityName:)`. It has no knowledge of the entity in that case since the initializer takes only a string. And, if you explicitly typecast the other statement `as! NSFetchRequest`, it also works fine. BUT, that generally is not necessary. This always works: `let fetchRequest: NSFetchRequest = MyEntity.fetchRequest()` Maybe, it's all just a quirk and it doesn't matter. – salo.dm Dec 14 '16 at 01:41