If you know anything about Core Data, you’re probably aware of Xcode’s built-in model editor. If you’ve used Core Data, you’ve probably spent more time with it than you care to remember. It edits the data model, which then gets compiled to a binary form Core Data can use when your app runs. Conveniently it can also generate some code for you, subclasses of NSManagedObject
for each entity in the model.
But what about the model itself? Where’s the code for that?
There isn’t any. The model editor uses a similar approach as editing UI in storyboards. Xcode creates the objects in memory and uses NSCoding
to write everything to a binary property list. When the app runs, the framework loads that property list, and like magic, all the same objects exist in your app.
With user interface there’s long been a back and forth between people who prefer working graphically and those who want to do it all in code– often because there seems to be too much magic going on behind the scenes. What about Core Data? Can you skip the model editor and write the code yourself?
You can! The model editor, like the storyboard editor, is a convenience but not a requirement. You can do it all in code. If you really want. If you look at the documentation for NSManagedObjectModel
, you’ll see that the collection of entities in the model is declared as
var entities: [NSEntityDescription] { get set }
Likewise the docs for NSEntityDescription
show that an entity’s properties are declared as
var properties: [NSPropertyDescription] { get set }
So what? So they both have set
in the declaration. They can be changed. This is not an accident. You can create and configure your entity descriptions in code, and then instantiate an NSManagedObjectModel
that uses them. From there, it’s Core Data as usual.
A Simple Model
Let’s give this a try. I’ll use a simple model with two entities, each with one attribute, and with a single relationship between them. If we were using the model editor it might look like this:
Or if you prefer the graphical style, like this:
First I’ll write the code for the custom classes I’ll be using. Two entities, one attribute each, with a relationship.
@objc(Event)
class Event: NSManagedObject {
@NSManaged var timestamp: Date?
@NSManaged var location: Location?
}
@objc(Location)
class Location: NSManagedObject {
@NSManaged var name: String?
@NSManaged var events: NSSet?
}
Here I’m declaring attributes and relationships the same way that Xcode does when generating subclasses. That’s not absolutely necessary. But if you do things differently, be sure you understand the consequences, because Xcode doesn’t do it this way by accident. For example, I could declare the events
relationship as Set<Event>
to be more Swifty, but that would affect how Core Data handles data faulting.
Creating the Entity Descriptions
This isn’t the model though, at least as far as Core Data is concerned. These subclasses are handy, but we have some work to do. First, here’s the Event
entity and its timestamp
attribute:
let eventEntity = NSEntityDescription()
eventEntity.name = "Event"
eventEntity.managedObjectClassName = "Event"
let timestampAttribute = NSAttributeDescription()
timestampAttribute.name = "timestamp"
timestampAttribute.type = .date
eventEntity.properties.append(timestampAttribute)
That’s right, you can create entitie descriptions and attribute descriptions in code. Then you can configure them with code that matches every checkbox and pop-up menu in the model editor.
The managedObjectClassName
property for NSEntityDescription
controls the mapping from Core Data entity to NSManagedObject
subclass. This is how Core Data will know that when I use the Event
entity, it should use that class I created earlier.
All this stuff and more can be done in code:
The Location
entity and its attribute is similar:
let locationEntity = NSEntityDescription()
locationEntity.name = "Location"
locationEntity.managedObjectClassName = "Location"
let nameAttribute = NSAttributeDescription()
nameAttribute.name = "name"
nameAttribute.type = .string
locationEntity.properties.append(nameAttribute)
Now for the relationships. Just like the model editor, you need to create the relationship on both sides and make them inverses. The Event
-to-Location
relationship is to-one, and looks like this:
let eventToLocation = NSRelationshipDescription()
eventToLocation.name = "location"
eventToLocation.destinationEntity = locationEntity
eventToLocation.maxCount = 1
eventEntity.properties.append(eventToLocation)
As with the attribute description above, every setting from the model editor is available.
The maxCount
property is a little under-documented and works a little differently from the model editor. In Xcode there’s a pop-up menu to choose to-one or to-many. If you choose to-many, there are also text fields where you can set min and max counts for the relationship.
In code it’s a little different. There’s a property on NSRelationshipDescription
called isToMany
that looks like the right thing, but it’s a read-only property. How do you configure the relationship, then? In code the rule is
- If
maxCount
is 0, it’s a to-many relationship with no upper bound. - If
maxCount
is 1, it’s a to-one relationship. - If
maxCount
> 1, it’s to-many with whatever upper bound you choose.
It’s like this because Core Data was written long before Swift, and has no way for maxCount
to be an optional value. Strangely maxCount
is unsigned, so negative values are permitted. Using negative numbers has the same effect as setting maxCount
to 0.
The Location
-to-Event
relationship is similar. Once it exists, I can make these two relationships inverses of each other:
let locationToEvent = NSRelationshipDescription()
locationToEvent.name = "events"
locationToEvent.destinationEntity = eventEntity
locationToEvent.maxCount = 0 // 0 is the default but this makes it clear
locationEntity.properties.append(locationToEvent)
eventToLocation.inverseRelationship = locationToEvent
locationToEvent.inverseRelationship = eventToLocation
As with the model editor, inverses need to be configured at both ends of the relationship.
On to the Data Model
Whew! OK that was all interesting, but now we need a data model to tie it all together.
let model = NSManagedObjectModel()
model.entities = [eventEntity, locationEntity]
Oh wait, that was easy. Instantiate the model and give it some entities. Now we have a working data model! Let’s use it.
Stop Me if You’ve Seen This Before
From this point on it’s nearly the same as when you’re using the model editor. The only significant difference is creating a persistent container that uses the model I just created instead of looking for one in a file.
let container = NSPersistentContainer(name: "My Model", managedObjectModel: model)
From there, set whatever container options you need and call container.loadPersistentStores
. And then it’s Core Data business as usual.
About those Writeable Properties
Don’t get too power-mad with code like this. There’s one major detail to be aware of. If you look at the docs for these writeable properties I’ve been using to build the model, you’ll see this message:
Setting this property raises an exception if the receiver’s model has been used by an object graph manager.
That means you can define the model in code, but you’re asking for trouble if you try changing it after you’ve used it for a Core Data stack. In the code above, they’re writeable right up to the point where I created the persistent container. After that, changing the model in any way will crash the app immediately.
Model Migration
What if you need to change your data model? I have good news! Probably. For most cases.
If your model changes are compatible with automatic lightweight migration, just change the model and it’ll work. Let’s say I decide to add a new title
attribute to Event
. First I’ll change the class to this:
@objc(Event)
class Event: NSManagedObject {
@NSManaged var timestamp: Date?
@NSManaged var title: String?
@NSManaged var location: Location?
}
And I’ll add a few lines to create the attribute:
let titleAttribute = NSAttributeDescription()
titleAttribute.name = "title"
titleAttribute.type = .string
eventEntity.properties.append(titleAttribute)
Now what happens if I try to use the model with an existing data store? If shouldMigrateStoreAutomatically
and shouldInferMappingModelAutomatically
are both true
, the migration is automatic and the new model works with the existing data. Both of those attributes are true
by default, so unless you changed them, they’re already true.
Automatic lightweight migration covers a bunch of common cases. Check out Apple’s documentation for a full list.
If you can’t use automatic lightweight migration, well, you are in for a bit of work. If you didn’t use the data model editor, you can’t have Xcode set up a migration manager for you. But if you’re writing your model in code, you probably don’t want to, because that’s even deeper magic than the model editor.
As with the model, you can do it all in code, but as with code, it means working in some rarely used corners of the Core Data framework. The details are more than I’m going to get into here, but whatever you do, keep the code for the old model because you’ll need both old and new data models to do the migration. Then, investigate NSMappingModel
and NSMigrationManager
to work out the details.
Why (Not) Do This?
I’ve never done this in a real app. But then, I’m also the kind of developer who uses interface builder for UI design whenever possible. There’s more than one way to do it, though. If you prefer to keep your UI in code, maybe you’d prefer to do Core Data that way as well, for more or less the same reason. I can see that version diffs would probably be easier to read when changing the model. It’s not for me, but that’s my preference. Maybe you’d like this way better.
The biggest hurdle is probably if you ever need a model migration that can’t be done automatically. The model editor has a built-in convenience for keeping older models around and indicating which is current. You’d have to tackle that yourself by keeping code for old models around, on top of needing to write the migration as mentioned above.
Additional Note
Added Nov 10
After posting the above, Michael Tsai responed via Twitter:
So there’s one more potential caveat: If you hear about a new Core Data feature you want to use in your model, it might not be immediately available in code. That would be a good reason to file an issue with Apple, but in the meantime you’re stuck either not using the new feature or resorting to undocumented API.