CoreData stack

CoreData framework can be used on both iOS and Mac devices. SQLite databases produced on Mac can be loaded and used on iOS (at least so far). And in general, CoreData supports 4 different types of persistent stores:

  • SQLite database

  • In-Memory storage

  • Binary

  • XML - this one is not available on iOS

So let's start with the stack. At the bottom, we have Persistent Store - the main storage of data. In the case of SQLite, we can treat it as real SQLite files. Then above we have Persistent Store Coordinator, which is initialised with a Model that defines CoreData objects structure. A Persistent Store Coordinator can control multiple Persistent Stores. And above it, we have Managed Object Context. It is possible to connect multiple Managed Object Contexts to one Persistent Store Coordinator and also Managed Object Contexts may have other child contexts. And finally, contexts contain Managed Objects.

Firstly, let's see how we can add a data model to the project. This can be done in the Xcode in the following way: File -> New -> File... and then select Data Model. After the model is added we can open it and start adding objects. Also, we can add additional configurations if needed.

Each configuration will represent a separate persistent store. After the new configuration is added we need to move all the required entities to it from the "Default" one. So in the example above we added a new configuration and named it "CoreDataExample". When we need to create multiple persistent stores we have two options - the first one is to have one model and multiple configurations, and the second one is to have multiple models with one configuration for each store. I prefer the second option because when an object in the data model is changed all persistent stores connected to this data model will be forced to migrate data, even if this object does not present in their configurations.

Next, we need to create a persistent store coordinator in code with our data model:

guard let dataModelUrl = Bundle.main.url(forResource: "Model", withExtension: "momd") else {
    //CoreDataError is a custom internal error
    throw CoreDataError.dataModelNotFound
}
let objectModel = NSManagedObjectModel(contentsOf: dataModelUrl)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: objectModel)

When a coordinator is created we can add a persistent store to it. To do that we need to provide the following parameters:

  • Store type. In our case, we will use SQLite.

  • Configuration name. Above we added a configuration with the name "CoreDataExample"

  • Location. Location on the disk where SQLite file will be created. CoreData uses journaling in SQLite by default so there will be created 3 files: "StoreName.sqlite", "StoreName.sqlite-shm", "StoreName.sqlite-wal"

  • Options. In our example, we will provide 2 options that will allow CoreData to automatically perform lightweight migrations when the model is changed.

let location: URL = ...
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
                                   configurationName: "CoreDataExample",
                                   at: location,
                                   options: [
                                    NSMigratePersistentStoresAutomaticallyOption : true,
                                    NSInferMappingModelAutomaticallyOption: true
                                   ])

Now we can create managed object contexts. Below is a list of parameters that I usually use for managed object context creation:

  • NSManagedObjectContextConcurrencyType. There are 2 of them:

    • .mainQueueConcurrencyType - we should use it in case we need to access managed objects from the main thread.

    • .privateQueueConcurrencyType - in this case, CoreData will use a private background thread to create and process managed objects.

  • We need to link our persistent store coordinator to our new context.

  • We need to choose NSMergePolicy in case of conflicts between managed objects in the context:

    • .error - CoreData will fail to save objects and will provide error details.

    • .mergeByPropertyStoreTrump - CoreData merges by the individual property with external changes trumping in-memory changes

    • .mergeByPropertyObjectTrump - CoreData merges by the individual property with in-memory changes trumping external changes

    • .overwrite - CoreData will save the in-memory version to the store

    • .rollback - CoreData will discard the in-memory changes and will keep the persistent version of the data

  • In the case of large databases, I suggest nullifying undo manager because it consumes a lot of memory and processing time so CoreData works much slower when it is enabled.

let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = coordinator
context.mergePolicy = .rollback
context.undoManager = nil

In case we need to use multiple managed object contexts we can create them all assigned to a persistent store context.persistentStoreCoordinator = coordinator or as a parent-child contexts childContext.parent = parentContext. In the next post, we will compare both approaches to better understand the pros and cons for each of them.

And finally, we can create managed objects in the context, populate them and then save to the disk. A quite important thing is to check if any parameter of the managed object was changed before setting it because even if we set it to the same value CoreData will mark object as dirty and will perform saving opetation to the disk. In later posts I will explain how to improve CoreData performance including these things above.