Creating Core Data Objects In Swift

August 1, 2015

Core Data relies heavily on string-based APIs, which makes it cumbersome to create type safe code. Most developers end up creating factory methods, macros and other conveniences to improve working with Core Data.

I like to use an Objective-C Category method on NSManagedObject to retrieve the entity name of an NSManagedObject subclass instance. That name is used to make it easier to deal with Core Data's string-typed API.

Let's first take a look at some Objective-C code. Then jump over to Swift.

Objective-C Category

@import CoreData;

@interface NSManagedObject (EntityAdditions)

+ (NSString *)hrl_entityName;

@end
#import "NSManagedObject+EntityAdditions.h"

@implementation NSManagedObject (EntityAdditions)

+ (NSString *)hrl_entityName
{
    return NSStringFromClass([self class]);
}

@end

Using NSStringFromClass works because the entity and class have the same name. Also, the code presented in this post assumes that the entity is not associated with a module.

Example Objective-C Usage

Let's assume there is a Core Data entity named HRLEngine whose class name is also HRLEngine. Here is how to use the category method to insert a new engine into a managed object context.

NSString *entityName = [HRLEngine hrl_entityName];
HRLEngine *engine = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:managedObjectContext];

This is common Core Data boilerplate code. All we did was make it slightly more type-safe by using the hrl_entityName category method.

For the most part this works well. However, we can do better.

Let's look at two possible solutions in Swift - NSManagedObject convenience initializer - Generified "factory" method on NSManagedObjectContext

Swift API Goal (By Example)

The goal is to use a type-safe API to create a new instance of a Core Data entity in a given managed object context.

Convenience Initializer

var engine: Engine(managedObjectContext: managedObjectContext)

Factory Method On NSManagedObjectContext

var engine: Engine = managedObjectContext.insertObject()

Both solutions insert a new Engine into the the managed object context.

Swift API Goal Solutions

Deriving The Entity Name

Here's a Swift Extension that mirrors the Objective-C Category defined above. We'll use the fact that the entity and class are the same. It may seem strange to use NSStringFromClass in Swift. Perhaps there's a better way to do "dynamic" programming in Swift 2 that I do not yet know about.

extension NSManagedObject {

    public class func entityName() -> String {
        // NSStringFromClass is available in Swift 2.
        // If the data model is in a framework, then
        // the module name needs to be stripped off.
        //
        // Example:
        //   FooBar.Engine 
        //   Engine
        let name = NSStringFromClass(self)
        return name.componentsSeparatedByString(".").last!
    }
}

Convenience Initializer

extension NSManagedObject {

    convenience init(managedObjectContext: NSManagedObjectContext) {
        let entityName = self.dynamicType.entityName()
        let entity = NSEntityDescription.entityForName(entityName, inManagedObjectContext: managedObjectContext)!
        self.init(entity: entity, insertIntoManagedObjectContext: managedObjectContext)
    }
}

The key to the convenience initializer is self.dynamicType.entityName().

The value returned from dynamicType represents the NSManagedObject subclass type of the managed object being created. In this case Engine.

The entityName() function can then be called because the type "is-a" NSManagedObject.

Therefore the entityName, in this example, is "Engine". The convenience initializer then calls the managed object's designated initializer, which completes the creation and insertion of an Engine into the managed object context.

Examples

var engine = Engine(managedObjectContext)
var turnout = Switch(managedObjectContext)
var whiskerTrack = WhiskerTrack(managedObjectContext)

Factory Method On NSManagedObjectContext

import CoreData

extension NSManagedObjectContext {

    public func insertObject<T: NSManagedObject>() -> T {
        guard let object = NSEntityDescription.insertNewObjectForEntityForName(T.entityName(), inManagedObjectContext: self) as? T
            else { fatalError("Invalid Core Data Model.") }
        return object;
    }
}

Examples

let engine: Engine = managedObjectContext.insertObject()
let turnout: Switch = managedObjectContext.insertObject()
let whiskerTrack: WhiskerTrack = managedObjectContext.insertObject()

The compiler generates an error:

let foo: NSString = managedObjectContext.insertObject()
// 'NSManagedObject' is not convertible to 'NSString'

This will compile but fail at runtime:

let foo: NSManagedObject = managedObjectContext.insertObject()

We can help the compiler by improving the insertObject method. More on that in a future post.

Wrap Up

The two solutions currently work with Xcode 7 beta 4.

I personally like the convenience initializer solution. First, it is a natural API. Second, it allows for fully realizing an object in a single initialization call. Here's an example.

extension Engine {

    convenience init(name: String, roadNumber: String, managedObjectContext: NSManagedObjectContext) {
        self.init(managedObjectContext: managedObjectContext)

        self.name = name
        self.roadNumber = roadNumber
    }
}

Example Usage

var engine = HRLEngine(name: "Missouri Pacific SD40", roadNumber:"3007", managedObjectContext: managedObjectContext)

// if you log the engine...
<Engine: 0x7fb1216e8440> (entity: Engine; id: 0x7fb1216d71f0 <x-coredata:///Engine/t16348995-64B1-43E1-8569-56E818D12F384> ; data: {
    name = "Missouri Pacific SD40";
    roadNumber = 3007;
    ...
})

There are plenty of existing Objective-C Core Data "helpers" floating around. I am sure there will be just as many in Swift in the years to come.