After my recent NSFetchedResultsController posts, Benedict Cohen brought a related issue to my attention. He complained that if a controller has a fetch predicate specified, and you merge in a change which makes some existing objects start matching the predicate, the controller may not spot those changes.
To break it down, let’s take a look at the simple standard merge technique Apple provide:
Work is performed in a secondary context, which is hooked up directly to the same persistent store coordinator as your main context. When finished, the secondary context is saved, and the code above used to “merge” those changes into the man context.
What is the merge actually doing? It’s bringing the main context up-to-date with what’s now in the store as a result of saving the secondary context. That’s done by processing the 3 types of changed objects from the secondary context:
- Inserted objects are faulted into the main context, in a fashion which will cause them to be reported as inserted
- Deleted objects are deleted from the main context also
- Updated objects are refreshed
NSFetchedResultsController and other interested parties observe NSManagedObjectContextObjectsDidChangeNotification so that after the merge they hear about the changes it brought about.
But there’s a catch. It turns out that mergeChangesFromContextDidSaveNotification tries to be a bit clever about handling updated objects. It seems that for updated objects which are faults in the main context, they’ll be left untouched.
On the surface this seems quite a good optimisation. If the object isn’t faulted into the context, it seems fair to assume nothing using that context cares about the object. And if there’s thousands of updates being merged, not having to process them all could be quite a saving.
If there are objects not faulted in because they failed to be of interest previously, but after the merge they should now be of interest, the optimisation is broken. It’s quite easy for objects to change such that they now match a fetched results controller’s predicate, but for the controller to go uninformed of that change.
Fortunately there is a fix, by defeating that optimisation:
Benedict feels this is a bug and has filed rdar://17569098 accordingly.
I’ve been giving it some thought and, while the current behaviour is less than ideal, it doesn’t quite seem to be a bug as such. Either Apple make merges match my workaround above, and risk heavily increased memory usage, or they leave it be.
But I think there’s a third option: I’d love to see the ability in Core Data to subscribe to a fetch request. You would be able to execute a fetch request and then be informed over time as the results of that request change. We’re basically talking the core of NSFetchedResultsController (i.e. not the sectioning aspect) implemented directly in Core Data, cross-platform so it’s available on the Mac too. The outcome should be that Core Data is now aware of “active” fetch requests, and so can handle those properly during merges. rdar://17588484
I think it’s interesting that HealthKit has a concept very similar to this with HKObserverQuery. Fingers crossed something like that shows up in Core Data soon.