Fetching and Observing a Single Object in Core Data

This article uses Swift 2.3.

I know what you're thinking after reading the title. Is Core Data really THAT complicated to need a whole article about working with a single managed object? Well, it sure looks so if you want to do it well.

Let's assume we're building a Twitter client for iOS using Core Data as a caching mechanism. We want to have a view controller showing a user profile. We'd like to have the same behavior as Twitter and Tweetbot have which is:

  • When we first open some user's profile we want to see a mostly blank view filling with information as soon as we get responses from the API.
  • The next time we open a profile for that user, we want the information to appear immediately as it's already present in our Core Data-backed cache. We still want to trigger a network request, though, to make sure that our local cache is up-to-date.

Profile Screen in Tweetbot

Basics

Let's start by splitting the problem into two parts:

  1. Getting the data from the local cache when opening a view controller.
  2. Refreshing the data when a network request finishes and the local cache is updated.

For the first task we can simply use an NSFetchRequest:

let fetchRequest = NSFetchRequest(entityName: "Profile")
fetchRequest.predicate = NSPredicate(format: "username = %@", userName)

do {
    let profiles = try managedObjectContext.executeFetchRequest(fetchRequest)
    assert(profiles.count < 2) // we shouldn't have any duplicates in CD

    if let profile = profiles.first as? Profile {
        // we've got the profile already cached!
    } else {
        // no local cache yet, use placeholder for now
    }
} catch {
    // handle error
}

This was easy, just a regular boilerplate for performing a fetch request. The second part — refetching when the fresh data comes from the API — is more interesting. It's fair to assume that we have some reference to an object that performs the network fetch and we know when it completes its job. In this case, we can just execute the same fetch request again and be done.

It Was Supposed to Be Easy

This is short-term thinking, though. There'll be cases when a user's profile updates because of other network operations in our app. If that happens we'll end up with a stale data visible on the screen.

The recommended approach to working around that in Core Data are notifications. We can subscribe to NSManagedObjectContextObjectsDidChangeNotification and receive notifications that contain a dictionary mapping from change reasons to sets of changed managed objects:

  • NSInsertedObjectsKey – set of inserted objects (when we didn't have a profile present in Core Data store before)
  • NSUpdatedObjectsKey – set of updated objects (when the profile changed)
  • NSDeletedObjectsKey – set of deleted objects (when the profile was deleted)

To sum up, we have two possible paths:

  1. On the first fetch, we get a managed object from the persistent store. When listening to notifications we can compare its objectID with those of objects provided in the notification.
  2. On the first fetch, we don't get a managed object from the persistent store as it's not there yet. So, when listening to notifications we can't use objectID to compare the objects. We have to do that in some different way.

There's a dissonance here. We have to handle initial fetch and future updates in (two) completely different ways. I don't like this! It's not just me too, as it's against current development practices, such as FRP.

Leveraging NSFetchedResultsController?

When I started writing this article I planned to show how to use NSFetchedResultsController to elegantly handle the use case we're discussing. However, while writing a wrapper for it I started feeling that I'm going against the grain. Even though my code was compliant with the documentation:

This class is intended to efficiently manage the results returned from a Core Data fetch request.

(...)

This class is tailored to work in conjunction with UITableView, however, you are free to use it with your own views.

I was noticing ugly issues with requirements, such as providing at least one sort descriptor for NSFetchRequest used by NSFetchedResultsController. So, I went back to the drawing board (i.e. went work a walk in a park) and made an important observation.

NSPredicate 👏

Searching for objects in Core Data is easy: we just set an NSPredicate instance on NSFetchRequest object. Searching for an object in a Set<NSManagedObject> seems like a completely different thing.

But it isn't!

Since NSPredicate is based on Objective-C's dynamism (KVC to be exact) we can leverage one more Objective-C API. With NSSet.filteredSetUsingPredicate(_:) we're able to use the same predicate both for NSFetchRequest and for filtering objects coming in a notification. There's no reason to stay away from Objective-C features when working with Objective-C frameworks.

Generalized Solution

Creating a generic NSFetchedResultsController alternative optimized for a single object use is within our reach now. Let's name it SingleFetchedResultController and start with its public interface:

public class SingleFetchedResultController<T: NSManagedObject where T: EntityNameProviding> {

    public typealias OnFetch = ((T, ChangeType) -> Void)

    public let predicate: NSPredicate
    public let managedObjectContext: NSManagedObjectContext
    public let onFetch: OnFetch
    public private(set) var object: T? = nil

    public init(predicate: NSPredicate, managedObjectContext: NSManagedObjectContext, onFetch: OnFetch)

    public func performFetch() throws
}

SingleFetchedResultController is initialized with three parameters: predicate, managedObjectContext and onFetch closure. First two are self-describing. onFetch closure is used instead of a delegate pattern here, mostly because of its simplicity. It'll be called each time an object property changes. This allows us to:

  • hide the implementation details of Core Data
  • keep the code updating the UI in one place; no matter if it's the first or any of the following updates

SingleFetchedResultController is also generic over NSManagedObject conforming to EntityNameProviding protocol:

public protocol EntityNameProviding {
    static func entityName() -> String
}

This introduces some type safety to Core Data code which by default is stringly typed. If we assume that names of entities match names of NSManagedObject subclasses, we can even implement it in a protocol extension:

extension NSManagedObject: EntityNameProviding {
    public static func entityName() -> String {
        return String(self)
    }
}

We won't be going through the whole implementation of SingleFetchedResultController here as it's not that interesting. Let's focus instead on its most important part:

@objc func objectsDidChange(notification: NSNotification) {
    updateCurrentObjectFromNotification(notification, key: NSInsertedObjectsKey)
    updateCurrentObjectFromNotification(notification, key: NSUpdatedObjectsKey)
    updateCurrentObjectFromNotification(notification, key: NSDeletedObjectsKey)
}

private func updateCurrentObjectFromNotification(notification: NSNotification, key: String) {
    guard let allModifiedObjects = notification.userInfo?[key] as? Set<NSManagedObject> else {
        return
    }

    let objectsWithCorrectType = Set(allModifiedObjects.filter { return $0 as? T != nil })
    let matchingObjects = NSSet(set: objectsWithCorrectType)
        .filteredSetUsingPredicate(self.predicate) as? Set<NSManagedObject> ?? []

    assert(matchingObjects.count < 2)

    guard let matchingObject = matchingObjects.first as? T else {
        return
    }

    object = matchingObject
    onChange(matchingObject, keyToChangeType(key))
}

In updateCurrentObjectFromNotification(_:key:) we:

  1. Get allModifiedObjects from NSNotification.userInfo dictionary.
  2. Create a set consisting only of objects that have the correct type.
  3. Filter those objects using the predicate that's also used for our NSFetchRequest.
  4. Check if there's a matching managed object.
  5. Update object property and call onChange closure if needed.

You can check the whole implementation on GitHub here.

Conclusion

We analyzed different ways of fetching and observing changes of a single managed object. Then, we unified the logic of fetching and listening to changes which will allow us to update the UI with ease. Finally, we extracted this logic into a separate component: SingleFetchedResultController.

Thanks to Kamil Kołodziejczyk for reading drafts of this.