Mastodon

Core Data Using Only Code

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:

Core Data model editor showing two entities in table style

Or if you prefer the graphical style, like this:

Core Data model editor showing two entities in graph style

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.

To-many relationship detail from Xcode data model editor

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:

I prefer to do it in code, but unfortunately the API usually seems to lag what you can do in the model editor. So at various times it’s been necessary to use private API to enable indexing, persistent history tracking, and other features that were new.

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.