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.