Thursday, July 2, 2015

NSURLSession & CoreData

I just moved one my apps from using a NSURLConnection to using a NSURLSession for REST requests, as this permits background data updates (since my app provides ambient cueing, it’s critical that it run the background). I had more difficulty doing this than expected. This was partly because it involved moving from a single threaded to multi-threaded operational environment, but also because I didn’t completely understand the purpose of a NSManagedObjectContext nor the difference between that and the NSPersistentStoreCoordinator (my lack of understanding might be due, in part, to its dynamic nature — from Apple’s doc: This is a preliminary document for an API or technology in development.)

My concerns were simple: what code drives the process, when does that code have control, and what governs the impact of the code’s execution (which turns out to be key in this situation, since is NOT necessarily governed by the process that has control).

My app has simple, relatively standard, behaviors around web services and CoreData (CoreData storage is in a SQLite database). The sequence is as follows:

On startup, the app initializes some persistent coreData structure as necessary.
It then updates configuration data from a web service (& persists it). A remnant of a previous design, it uses NSURLConnection.
A background thread is then started running an NSURLSession that gets new data from a web service, analyzes it, persists the data and should update the model used by the GUI.

My goal was to get the GUI to display the new data (in a timely manner, of course). I tried a few different strategies:

Just trying it with my current configuration settings didn’t work. Data initialized OK. Data from the background thread hit SQLite, but “spontaneous updating” of the data on the main GUI thread didn’t happen (I started using SQLite Pro a few months ago, It’s ability to see the data that’s hit the db has been a tremendous time saver).

I tried changing the setting for the main thread’s NSManagedObjectContext to NSMergeByPropertyStoreTrumpMergePolicy, which seemed like the behavior I wanted, didn’t help either (I reverted the change to keep myself in a known state).

It turns out that the accepted way to do this is to take the values provided by the save call that commits the changes, and then send them to the context that requires updating. There is a strong caveat, however: the receiving context must be NSMergeByPropertyStoreTrumpMergePolicy. I had a bit of trouble accepting at first. After all, the call to mergeChangesFromContextDidSaveNotification is coming from another context, not from the data store itself.

However, the implementation pattern belies that understanding: the source context is not necessarily responsible for the message to the “stale context”. Any selector posted to the NotificationCenter for the NSManagedObjectContextDidSaveNotification could be the source for the update, as the notification itself comes from the persistent store. A recommended pattern is for the thread responsible for committing the changes add/remove itself as an observer immediately around the save, as shown below.

NSNotificationCenter.defaultCenter().addObserver(self,selector: "notifySiblingContext:", name:NSManagedObjectContextDidSaveNotification, object: context)

NSNotificationCenter.defaultCenter().removeObserver( self, name:NSManagedObjectContextDidSaveNotification, object: context)

Where notifySiblingContext is the function that updates the stale context(s).

@objc func notifySiblingContext(notification: NSNotification){

In summary, the context performing holding the modified objects and initiating the save, should be set to NSMergeByPropertyStoreTrumpMergePolicy, while the context receiving the update should be set to the complementary NSMergeByPropertyStoreTrumpMergePolicy. The processing of the updates is effected by adding/removing the updating process as a context store observer immediately around the save. 

Notification of a successful save kicks off the update process for the sibling context(s).

Debugging tip: If the merge doesn't appear to doing anything, check that the context getting the mergeChangesFromContextDidSaveNotification message is the same as the context you're observing.