Modern Core Data in Swift

March 8, 2018 An opinionated guide to employing Core Data in a modern Swift app.

Having been released as early as 2005 alongside with Mac OS X Tiger, Core Data has come a long way. But despite Core Data’s age, Apple has applied serious modernization efforts during the last few years while also adding many new and exciting features.

Due to these modernizations you can now use Core Data with natural and concise Swift syntax. And—using the power of Swift protocols and extensions—one can make working with Core Data even funner!

This post is meant to be an update on Daniel Eggert’s awesome post from 2016 and serve as a reference on basic use-cases.

Setting Up the Stack

While it is important to know the how to set up Core Data manually, NSPersistentContainer is a welcome addition:

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "MyAwesomeModel")
    container.loadPersistentStores { (_, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }

        // Optionally enable automatic merging.
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
    }
    return container
}()

In just a few lines of code, you can load your model (i.e. schema) and store and initialize a store coordinator and view context. A few years ago, this alone would have been enough an entire tutorial!

Unless you are doing something like a document-based app, you should use one NSPersistentContainer per process. If you are interested in sharing a persistent store across multiple processes, e.g. a main app and a today extension, take a look at Persistent History Tracking in Core Data.

Defining Your Models

Creating Core Data entities in an .xcdatamodeld file actually hasn’t changed very much. What has changed, though, is defining your model classes in code since Apple finally added code generation for .xcdatamodeld files! Having selected an entity, you can see a new property in the Data Model Inspector: “Codegen”—with three options to choose from:

  1. Manual/None: Performs exactly as advertised by doing nothing (old behaviour)
  2. Class Definition: Generates NSManagedObject subclasses per entity in an additional build phase—no additional configuration necessary
  3. Category/Extension: Similar to the above, but generates Objective-C categories or Swift extensions that add NSManaged attribute properties for every entity

And yes, it works for both Objective-C and Swift! It also doesn’t pollute your workspace as the generated files live in “Derived Data” exclusively. If you would like to know more details, check out this post in Use Your Loaf.

There is a Catch

Personally however, I find there are four reasons for sticking with “Manual/None”:

  1. Either option prevents you from adding documentation to properties/entity attributes that shows up when option-clicking on them.
  2. You have no control over access modifiers—it’s all public.
  3. Xcode might generate to much; do you really need addToRelationship and removeFromRelationship for every relationship that is also represented by a property?
  4. This is the most important one for me: Type safety. By default, Xcode generates untyped NSSets for many-to-one and many-to-many relationships, which is a pity since Core Data supports Swift’s Set<Element>. Plus, every property—even relationships—will be wrapped Optionals, regardless of the optionality chosen in the Data Model Inspector.

Even for custom classes, Xcode still synthesizes and initializer that inserts an object into a context and .fetchRequest(), which is quite nice.

Example

Here is an example of how such a custom model class might look:

import CoreData

@objc(User)
final class User: NSManagedObject {
    @NSManaged var username: String
    @NSManaged var givenName: String
    @NSManaged var familyName: String
    @NSManaged var namePrefix: String?
    @NSManaged var nameSuffix: String?
    @NSManaged var organization: Organization
    @NSManaged var authoredCourses: Set<Course>
}

Notice the @objc(User)? This is needed for the Objective-C-Runtime to detect your class. @NSManaged is a Swift attribute that tells the compiler that Core Data will handle getting and setting this property, which allows for faulting and other optimizations.

And as you can see, you can use optional and non-optional types from the Swift Standard Library like String, Data, Int, Double, Date, or URL as well as other NSManagedObject subclasses and sets thereof.

If you’re like me and would like to use Swift’s Int be sure to select Int64 for the corresponding attribute!

Creating and Deleting Objects

Inserting a new object into an NSManagedContext couldn’t be easier than using init(context:):

let user = User(context: context)
user.username = "jony"
user.givenName = "Jonathan"
user.familyName = "Ive"
user.organization = apple

Deletion, however, has to be invoked on the context:

context.delete(anotherUser)

There is a Small Catch

Suppose you apply database normalization and and end up with a user state that has a one-to-one-relationship with a user. Of course, you could add user.state = UserState(context:) for every instantiation. However, this approach tends to be error-prone because its easy to forget or get wrong.

Instead, it would be better to implement custom initialization logic. Unfortunately, you loose synthesized boilerplate when overriding init(context:). This is why I would recommend conforming to a custom protocol such as CDCreatable, which basically adds an additional convenience initializer. You can find the code below :)

required convenience init(createIn context: NSManagedObjectContext) {
    self.init(context: context)
    state = UserState(createIn: context)
}

After adding this initializer and conforming to CDCreatable, User(createIn:) automatically creates a state for you. You can also add additional initialization parameters for properties or add more complex logic.

init(entity:insertInto:) will be invoked every time an instance is created, which also happens when fetching from Core Data. Custom initialization logic in this method should thus be as lightweight as possible.

As a bonus, CDCreatable conformance also synthesizes delete(in:) as syntax sugar for delete() on NSManagedObjectContext.

Playing Fetch

Just like creating objects, fetching them is pretty straightforward:

  1. Create a fetch request using the synthesized static method:
    let fetchRequest = User.fetchRequest()
  2. Customize it to fit your query and use-case:
    fetchRequest.predicate = NSPredicate(format: "organization == %@", apple)
    fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \User.username, ascending: true)]
  3. Execute it against a context:
    let users = try context.fetch(fetchRequest)

Note that—just as with many other Core Data APIs—you can use Swift 4’s new KeyPaths for constructing NSSortDescriptors! Awesome, right?

Please use NSPredicates and don’t ever fetch everything and then use Swift’s .filter to narrow down your results. This prevents Core Data from optimizing your query and might result in major performance problems.

Making it Even Less Verbose

Similar to the one synthesized by Core Data, I’ve added an extension that adds an additional static NSFetchRequestResult.fetchRequest that automatically infers the entity name, sets common properties, and returns a typed fetched request.

If you like, you can (optionally) also add convenience properties and methods to your object classes, e.g.:

extension User {
    static let defaultSortDescriptors = [
        NSSortDescriptor(keyPath: \User.username, ascending: true),
    ]
}
extension Organization {
    var usersFetchRequest: NSFetchRequest {
        let predicate = NSPredicate(format: "organization == %@", self)
        return User.fetchRequest(predicate: predicate, sortDescriptors: User.defaultSortDescriptors)
    }

    func fetchUsers(in context: NSManagedObjectContext) throws -> [User] {
        return try context.fetch(usersFetchRequest)
    }
}

Instead of the above, you can now simply write:

let users = try apple.fetchUsers(in: context)

As you can see, I like to separate the fetch requests and actual fetching in order to make working with NSFetchedResultsController as easy as fetching.

Switching Contexts

When you are modifying large amounts of objects or performing a background refresh from a server, you should avoid doing this on the persistent container’s viewContext as doing so will block the main thread and thus render your application unresponsive. Instead, consider using performBackgroundTask(), which creates a new background context that operates on a private background queue.

But it is possible that you might need an object from the view context e.g. to set a relationship with a new object in the private context.

Doing this will cause your app to crash: Contexts and managed objects should never be used outside their initial queue/context.

Luckily, Core Data manages an opaque .objectID for every object. This means you can re-fetch your object in another context in a very performant way and without custom fetch request using .object(with:).

I’ve written a type-safe utility extension that lets you access every object in another context as simple as user.in(privateContext), which you can also find below.

Unit Testing

There is not too much to say about unit testing in Core Data other than: Use the in-memory store instead of the default SQLite option if you want to save yourself from unnecessary I/O overhead. Just set .type to NSInMemoryStoreType on your store description on initialization.

It can also be useful to create convenience constructors for your object classes, which makes creating mock data a little bit more concise. You will also have to consider whether you want to .reset() your context in the tests’ set-up method or create it from scratch. Keep in mind that the latter option will be more performance-expensive.

Code

Thanks for reading—I hope you’ve enjoyed my tips on using Core Data in Swift!

Here is the code I’ve promised :)