Developers familiar with Core Data don't consider the framework difficult or hard to work with. If you respect the rules of the framework, Core Data won't surprise you. But that is often the problem. Developers make mistakes because they violate a rule they don't know about. In this article, I discuss three common mistakes developers make working with Core Data.

Accessing a Managed Object Context From the Wrong Thread

Core Data was never designed to be used in a multithreaded environment. Fortunately, the framework has evolved dramatically over the years and it is not difficult to use Core Data in multithreaded applications.

If you are familiar with Core Data, then you know that Core Data uses thread confinement to protect a managed object context. This means that you should only access a managed object context from the thread it is associated with. But how do you figure out which thread that is?

As of iOS 5 and macOS 10.7, a managed object context creates and manages a dispatch queue on which it performs its work. The NSManagedObjectContext class provides a convenient interface for performing operations on the dispatch queue of the managed object context. You have two options:

  • perform(_:)
  • performAndWait(_:)

Each method accepts a closure in which you can perform operations on the managed objects that are managed by the managed object context.

managedObjectContext.perform {
    ...
}

managedObjectContext.performAndWait {
    ...
}

What sets perform(_:) and performAndWait(_:) apart is how the chunk of work is scheduled on the dispatch queue of the managed object context.

The perform(_:) method asynchronously performs the chunk of work on the dispatch queue of the managed object context. As the name implies, performAndWait(_:) synchronously executes the closure that is passed as an argument on the dispatch queue of the managed object context.

Unless you are absolutely certain you are accessing a managed object context from the proper thread, you are encouraged to use the perform(_:) and performAndWait(_:) methods to avoid running into threading issues.

Passing Managed Objects Across Threads

Another common mistake developers makes is passing managed objects from one thread to another. This is not uncommon if the application fetches data from a remote backend, for example. The rule you need to respect is simple. You should never pass a NSManagedObject instance from one thread to another. The NSManagedObject class is not thread-safe. Period.

While this may seem inconvenient, the solution is simple. The Core Data framework provides a solution to pass managed objects from one thread to another, the NSManagedObjectID class. An instance of this class uniquely identifies a managed object in an application. What is important for this discussion is that the NSManagedObjectID class is thread-safe.

Instead of passing a managed object from one thread to another, you pass a NSManagedObjectID instance. You can ask a NSManagedObject instance for its unique identifier by accessing the value of its objectID property.

let objectID = managedObject.objectID

A managed object context knows how to fetch a managed object if you hand it an instance of the NSManagedObjectID class. In fact, the NSManagedObjectContext class provides several methods for fetching the managed object that corresponds with the NSManagedObjectID instance it is given.

  • object(with:)
  • existingObject(with:)
  • registeredObject(for:)

Each of these methods accepts an instance of the NSManagedObjectID class.

let objectID = managedObject.objectID

DispatchQueue.main.async {
    ...

    let managedObject = mainManagedObjectContext.object(with: objectID)

    ...
}

The first method, object(with:), returns a managed object that corresponds to the NSManagedObjectID instance. If the managed object context does not have a managed object for that object identifier, it asks the persistent store coordinator. This method always returns a managed object.

Know that object(with:) throws an exception if no managed object can be found for that object identifier. For example, if the application deleted the record corresponding with the object identifier, Core Data is unable to hand your application the corresponding record. The result is an exception.

The existingObject(with:) method behaves in a similar fashion. The main difference is that the method throws an error if it cannot fetch the managed object corresponding with the object identifier.

The third method, registeredObject(for:), only returns a managed object if the record you are asking for is already registered with the managed object context. In other words, the return value is of type optional NSManagedObject?. The managed object context does not fetch the corresponding record from the persistent store if it cannot find it.

The object identifier of a record is similar, but not identical, to the primary key of a database record. It uniquely identifies the record and enables your application to fetch a particular record regardless of what thread the operation is performed on.

Skipping the Basics

The most common mistake developers make, by far, is skipping the fundamentals of the Core Data framework. Developers simply don't take the time to learn the ins and outs of the framework and they are surprised when they run into problems or the framework doesn't behave the way they expect.

Avoid the Struggle and Take Your Time

By taking the time to learn Core Data, you save yourself a lot of time and frustration. Core Data isn't UIKit or Foundation. You cannot pick and choose which components you want to use. This article shows that Core Data has a strict set of rules you should not break. If you take the time to learn those rules, you will be fine.

If you are committed to learning Core Data, then you should take my course, Mastering Core Data With Swift. In less than three hours, you learn what you need to know about the framework to integrate it in your projects. Core Data is not difficult. That is my promise to you.