Fetching and Observing a Single Object in Core Data
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.
Basics
Let's start by splitting the problem into two parts:
- Getting the data from the local cache when opening a view controller.
- 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:
- 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. - 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:
- Get
allModifiedObjects
fromNSNotification.userInfo
dictionary. - Create a set consisting only of objects that have the correct type.
- Filter those objects using the predicate that's also used for our
NSFetchRequest
. - Check if there's a matching managed object.
- Update
object
property and callonChange
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.