A Quick Intro to CoreData

There are two common misconceptions about Core Data. The first is that it is a “database” framework. It is not. It is an object framework that persists between loads and, in some cases, is backed by a database. But it is very important to understand that the “database” is not something that the programmer needs to know about. In fact, trying to treat it like a database can even cause Core Data’s second common misconception: that Core Data is difficult to use.

To begin, Core Data works via several core pieces: NSManagedObjectModel, NSPersistentStoreCoordinator, and NSManagedObjectContext, and NSManagedObjects. With knowledge of just these four items, Core Data can be easily added to any application.

In more complex cases, we may need to add NSMappingModel, NSMigrationManager, and NSPersistentStore to this list, but I will not be discussing them in detail today

Above, you can see a conceptual layout of the Core Data stack. From the top, data exists inside the NSManagedObjectContext in the form of NSManagedObjects. The NSManagedObjectContext is backed by NSPersistentStoreCoordinator which is built on an NSManagedObjectModel and the NSPersistentStores that are the actual representation of the data on disk. Setup moves (roughly) from the bottom to the top, but for the purposes of understanding Core Data, I will explain in a top-down fashion before we get to the actual setup, as the top is where most of your interactions will occur.

NSManagedObject

As we saw above, NSManagedObjects are the heart of Core Data. This is the data in object form. Due to the design of Core Data, all of your objects must be NSManagedObject (which can be subclassed).

As a side note NSManagedObject contains various methods and properties that Core Data relies on to persist data and should not be overridden (see the [documentation](https://developer.apple.com/reference/Core Data/nsmanagedobject) ).

Despite how you define your object model, NSManagedObject will not, by default, have the properties that the model defines. Think of the base NSManagedObject as something similar to an NSDictionary where each ‘dictionary’ key is pre-determined and KVO compliant. This means that our NSManagedObject’s keys are the properties we define in our object model. Simply put, the definition explains the name and data type of each key, as well as any relationships (i.e. references) to other NSManagedObjects.

Unfortunately, since there are no properties this means that we can immediately access them. The only way to access the "properties" on an NSManagedObject is to use value(forKey:) and setValue(_:forKey:). While this will work, it is generally more helpful to assign to real properties, right? By subclassing NSManagedObject we can define the object properties in the subclass. Xcode is also somewhat helpful in this respect, as it will generate the base class interface. We can, of course, add to this interface once it is generated, but those changes will be overwritten if we generate the object file again.

Once you understand how to access the data’s properties, the last thing you must remember is that NSManagedObject lives in memory, not in persistent storage. While the NSManagedObjectContext does live on top of a persistent stack, it does not write data to disk on every modification. We must explicitly save the parent NSManagedContext any time we would like to persist changes to our dataset.

NSManagedObjectContext

NSManagedObjectContext is the workhorse of Core Data. This class defines the memory space for all of the NSManagedObjects as well as providing the means of persisting data. Think of it as the stage where our actors, in the form of NSManagedObjects live. Whenever we want to add a new actor, we must do it by inserting into our NSManagedObjectContext via insert(_:) or NSManagedObject’s init methods. Similarly, the NSManagedObjectContext will keep objects alive until we call delete(_:). Setting an NSManagedObject to nil will not delete it from the NSManagedObjectContext or our data set.

Above, we saw that data is not automatically saved to disc. If NSManagedObjectContext is a stage, then that stage is live. If changes need to be persisted, save() must be called. There are two reasons that the context is not automatically saved to disc: it allows changes to be undone, and it increases performance. In the first case, methods such as undo(), reset(), and rollback() will allow the context to recover from incomplete data objects or if the user has made a mistake. Imagine a user that accidentally deleted their family’s contact information! Being able to undo that change would be a considerable plus.

Due to not being thread safe, NSManagedObjectContext can easily crash an application. In order to prevent this, make all changes within the perform(_:) or performAndWait(_:) methods.

Secondly, and as most programmers are aware, writing to disc is frequently the most time consuming task in computers, and an NSManagedObjectContext may have numerous changes at a time. If we add the fact that NSManagedObjectContext is not thread safe then we run into a potentially large problem if every minor change triggers a new disc operation. Since displaying data should be done on the main thread, that means we could potentially block the main thread while saving! On the other hand, imagine if these changes could be batched and saved when the user is not actively using the application? This is exactly why NSManagedObjectContext works like it does: it allows for individual fast memory-level changes, and then allows us to save all changes in a batch later on when it will not inconvenience the user (frequent options are applicationWillResignActive(_:) or applicationDidEnterBackground(_:)).

However, the NSManagedObjectContext still needs a little more help before it can be used. After all, there is no persistence in data without connecting to a file, right?

NSPersistentStoreCoordinator

Working back from the NSManagedObjectContext the next class is NSPersistentStoreCoordinator. This is the class that manages the persistent stores (as the name implies). More simply put, we use this class to handle reading and writing data to disc. This is also the class that will decide where and in what format (XML, SQLite, binary) our data will be saved.

Theoretically, this is a simple operation. Instantiate our coordinator, connect it to the context objects and data files, and… we’re done! Unfortunately, it is slightly more complex, though not much. While connecting to the contexts is simple, connecting to the data files can occasionally cause problems. Attaching a data file has the potential to take some time, so the an attach-on-boot method could potentially cause the app to crash if it takes too long.

The simple solution to this is to perform the attachment asynchronously using GCD or an Operation. Once the attachment is complete, we just need to update our interface.

NSManagedObjectModel

Taking one more step back, we need to design our data. When starting with Core Data, the first thing needed is an object model. This is simply a description of your data. You will define what types of objects exist, what data they will hold, and how they relate to each other. This will determine what entities / NSManagedObject subclasses you will be using and what they represent.

Example:

Working backwards from the object model, let’s go through an example. We will start with the "old" way (don’t worry it still works) as it should help you better link the pieces together. To begin with, this is the interface for creating object models in Xcode 12. Using the controls, we can add entities, edit their properties/class, and even see a graph view of how they relate to each other.

Next up, we need to instantiate this mapping into an object in our code:

let mapURL = Bundle.main.url(forResource: "MappingModel", withExtension: "momd")
let objectModel = NSManagedObjectModel(contentsOf: mapURL!)

Instantiating the model object is a fairly familiar process. In this case we have our model in the app’s bundle, but it could be located somewhere else.

Once the model object is instantiated, we need to start working on our persistentStoreCoordinator:

let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: objectModel!)

At this point, we have not yet connected the data, we have only told the coordinator what our data will consist of. We could attach the store and then instantiate the context, so we might as well do it now:

let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = persistentStoreCoordinator

In the above code, we have finally created and set our context. By using mainQueueConcurrencyType we have specified that the context will exist on the main thread.

It is also possible to create the context on a background thread using privateQueueConcurrencyType. For pre-iOS5 compatibility there is another option available, but it should only be used for compatibility.

Now that we have assembled most of the Core Data stack, we just need to attach our data file. As I mentioned before, it is better to do so asynchronously:

DispatchQueue.global(qos: .background).async {
    do {
        try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
                                                         configurationName: nil,
                                                         at: storeURL,
                                                         options: nil)
    } catch {
        // Handle error
    }

    DispatchQueue.main.async {
        // Callback
    }
}

Now we can begin using all the NSManagedObject subclasses we created in our app! You may be thinking that this is still a little difficult. And you are not alone! Apple also decided that such a setup was too much, so they added the NSPersistentStoreContainer in iOS 10. This is the "new" way of setting up a Core Data stack. This class rolls all the pieces of the previous stack into one class that also follows some best practices, like having a viewContext and backgroundContexts.

let container = NSPersistentContainer(name: "MappingModel")
container.loadPersistentStores { description, error in
    // Callback or Error Handling
}

While that is a lot less code, the way of using it is the same. The container.viewContext is still an instance of NSManagedObjectContext and is still the workhorse.

While I don’t expect everyone to come running back to Core Data now, I at least hope that you are a little more comfortable with it. You never know when you might need it!


*If you have any questions, comments, or corrections let me know on Twitter