read

Core Data has been around for 10 years, with many legacy concepts and APIs. This guide is the modern way to use Core Data, until further WWDC updates :)

If you are interested in the history of how we got here, the last section has the long history, describing the 3rd party stacks and libraries, and issues.

The technology has since improved much. In this guide, I will use show what a modern developer should use.

Create the database

Use Xcode to create the model schema. In this guide, the data model name (the .xcdatamodeld file) is “MyDataModel”.

When editing the entity, leave the entity name and class name the same.

Set Codegen to Class Definition.

Throughout this guide, we will use the following Note model as example:

@objc(Note)
public class Note: NSManagedObject { }
extension Note {
    @NSManaged public var content: String?
    @NSManaged public var priority: NSNumber?
}

There is also a generated fetchRequest with the Note type, which I have omitted above, but you should not remove in your code.

Setup the stack

public class DB {

    public static let `default` = DB()

    public var container: NSPersistentContainer!

    /// Call this once in `applicationDidFinishLaunching`
    public func setup(dataModelName: String) {
        container = NSPersistentContainer(name: dataModelName)
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Unable to load persistent stores: \(error)")
            }
        }
    }

}

We create a singleton class DB.default which we will use throughout the app. The setup must be run when the application is launched, with “MyDataModel” name.

It will crash if there is error – and yes, you want it to crash because most apps cannot continue without a working database.

If you have an advanced/unconventional use case, NSPersistentContainer has other inits that will allow you to customize the data model to load, and configure the persistent store.

CRUD

Create (aka save)

The steps in essence:

  1. Perform everything in a block with background context
  2. Create a new Note (NSManagedObject object) in the context
  3. Save the context
DB.default.container.performBackgroundTask { context in
    let note = Note(context: context)
    note.content = "Hello World"
    note.priority = 99
    try! context.save()
}

This is the modern way, which is much shorter than the past. If you don’t believe, take a look at Ray Wenderlich’s “updated guide” in 2017, which still uses the tedious way involving NSEntityDescription.entity and NSManagedObject(entity:insertInto:).

What happens when a context is saved? It will commit “one store up”, to either a parent context or the actual persistent store.

Read (aka fetch)

The steps in essence:

  1. Construct a fetch request (made up of predicates and sort descriptors)
  2. Use either viewContext (main thread) or newBackgroundContext()
  3. Call fetch with the fetch request
let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
fetchRequest.predicate = ...
fetchRequest.sortDescriptors = ...
let notes = try! DB.default.container.viewContext.fetch(fetchRequest)
notes.forEach {
    print($0.content)
}

Learning predicate will be another topic for another day. If you want to learn, you may refer to the documentation, guide and cheatsheet.

You could also call fetchRequest.execute() directly, which will automatically use the context associated to the current thread. Not recommended unless you are sure of the threads.

Update

Updating is simply mutating the models, then saving the context.

DB.default.container.performBackgroundTask { context in
    let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
    fetchRequest.predicate = ...
    let notes = try! context.fetch(fetchRequest)
    let note = notes.first
    note.content = "This is the first note"
    try! context.save()
}

The example above fetch notes in the background context, then mutate the first note and save.

Delete

Once again, use context to delete then save.

DB.default.container.performBackgroundTask { context in
    ...
    context.delete(note)
    try! context.save()
}

Note that if you need to delete all notes, you need to fetch all of them and calling delete for each. This is inefficient since you would need to load all the models, which is unnecessary in a delete. You could set includesPropertyValues to false in the fetch request. Another way is to use batch processing.

Dealing with concurrency

The introduction of NSPersistentContainer simplified Core Data framework, by making developer choose between these two contexts:

  1. viewContext is on main thread
  2. newBackgroundContext() or performBackgroundTask is on background thread

Container and contexts

The parent of both viewContext and newBackgroundContext() is the persistent store. As said before, when you save a context, it will commit to the parent.

When you save a background context, it will save to the persistent store, but it will NOT merge to the main context.

Often you would want your main context to reflect changes. To do that, you have to configure viewContext when setting up your database:

container.viewContext.automaticallyMergesChangesFromParent = true

If you perform save concurrently in multiple contexts, you could have merge conflicts. One way is to have an operation queue.

What happens to existing fetched objects when a merge happens? They are not affected. You need to refresh the changes by executing the fetch again.

How to know a context has changes? Observe posted notifications such as NSManagedObjectContextDidSave and deal with the inserted, updated and deleted objects.

What is read-only viewContext?

viewContext is a READ-only property.

You might be misguided to think it means that the context can only read, and cannot write, insert or save. Wrong! The read-only simply means the property cannot be set.

You can run viewContext.save, which will save the context on main thread. If you do so, just ensure that you are not running a very long operation otherwise the main thread will be blocked.

Pitfall: Faults

When you fetch models, sometimes there will be faults.

Faults are “unrealized objects”, designed to make Core Data efficient by avoiding needless fetching, until needed.

Faults are automatically resolved (fetched) when you access the property.

But if the “unrealized object” is somehow deleted, crash could occur. The simple configuration below makes those faults nil instead.

context.shouldDeleteInaccessibleFaults = true

Query Generation

A good time now to get to know this new feature in iOS 10 that prevents faults and crashes. Read this guide and watch WWDC 2016.

In essence, each context is pinned to a snapshot of the database.

By default, context are unpinned. You can start pinning with:

let token = context.queryGenerationToken
context.setQueryGenerationFrom(token)

At some point in time, you could move to the latest snapshot with NSQueryGenerationToken.current.

Other topics

NSFetchedResultsController manage the results of a fetch request, including changes to the objects in the context! A big topic so I leave in to another day. For now, you can read my 2015 guide.

Migration is unavoidable in app upgrade.

Back to the history..

I use Core Data way back in 2009 when iOS was first launch.

Why use Core Data at all? Because with Core Data API you can avoid writing SQL statements. That is the biggest benefit. A modern way to write database code.

But there are still lots of pain with this piece of Apple technology.

Over 10 years, it did improve, though some updates are long overdue considering it is a vital piece in the iOS/macOS stack.

  • 2009: Core Data in iOS 3
  • 2010: MagicalRecord is THE wrapper, was in Objective-C, but now dormant.
  • 2010: mogenerator is the third party model generator
  • 2015: CoreStore is in Swift and still updated, but might not be using the latest concepts
  • 2016: Biggest new set of features announced, including NSPersistContainer and much less verbose API

Back in the days.. my stack is to use MagicalRecord + mogenerator. There are many pitfalls eg. concurrency in managed object context, faults.

But Apple has fixed some quirks, at last.

In WDDC 2016, Apple has a pivotal release with the concept of NSPersistContainer, wrapping the creation of a database stack, and using that same container to access either a main context or a background context.

The managed object class generation is built in, with those sensible methods such as entity(). The use of Swift generic make type casting unnecessary.

Suddenly, Core Data seems much nicer to play with.

Yet there are more that can be improved.


Image

@samwize

¯\_(ツ)_/¯

Back to Home