Handling background changes with NSFetchedResultsController - iphone

I am having a few nagging issues with NSFetchedResultsController and CoreData, any of which I would be very grateful to get help on.
Issue 1 - Updates: I update my store on a background thread which results in certain rows being delete, inserted or updated. The changes are merged into the context on the main thread using the "mergeChangesFromContextDidSaveNotification:" method. Inserts and deletes are updated properly, but updates are not (e.g. the cell label is not updated with the change) although I have confirmed the updates to come through the contextDidSaveNotifcation, exactly like the inserts and deleted. My current workaround is to temporarily change the staleness interval of the context to 0, but this does not seem like the ideal solution.
Issue 2 - Deleting objects: My fetch batch size is 20. If an object is deleted by the background thread which is in the first 20 rows, everything works fine. But if the object is after the first 20 rows and the table is scrolled down, a "CoreData could not fulfill a fault" error is raised. I have tried resaving the context and reperforming the frc fetch - all to no avail. Note: In this scenario, the frc delegate method "didChangeObject...." is not called for the delete - I assume this is because the object in question had not been faulted at that time (as it is was outside the initial fetch range). But for some reason, the context still thinks the object is around, although is has been deleted from the store.
Issue 3 - Deleting sections : When the deletion of a row leads to the deletion of a section, I have gotten the "invalid number of rows in section???" error. I have worked around this by removing the "reloadSection" line from the NSFetchedResultsChangeMove: section and replacing it with "[tableView insertRowsAtIndexPaths...." This seems to work, but once again, I am not sure if this is the best solution.
Any help would be greatly appreciated. Thank you!

I think all your problems relate to the fetched results controller's cache.
Issue 1 is caused by the FRC using the cached objects (whose IDs have not changed.) When you add or remove an object that changes the IDs and forces an update of the cache but changing the attributes of an object doesn't do so reliably.
Issue 2 is caused by the FRC checking for the object in cache. Most likely, the object has an unfaulted relationship that persist in the cache. When you delete it in the background the FRC tries to fault in the object at the other end of the relationship and cannot.
Issue 3: Same problem. The cache does not reflect the changes.
You really shouldn't use a FRC's cache when some object other than the FRC is modifying the data model. You have two options:
(Preferred) Don't use the cache. When creating the FRC set the cache property to nil.
Clear the cache anytime the background process alters the data model.
Of course, two defeats the purpose of using the cache in the first place.
The cache is only useful if the data is largely static and/or the FRC manages the changes. In any other circumstance, you shouldn't use it because FRC need to check the actual data model repeatedly to ensure that it has a current understanding of the data. It can't rely on the object copies it squirreled away because another input may have changed the real objects.

My advice:
Detect the changes needed on the background thread
Post the changes to the main thread as a payload
Make the actual changes and save on the main thread (Managed Object Context on the main thread)
DO use the cache for the FRC; you'll get better performance
Quote from "Pro Core Data for iOS" by Michael Privat, Robert Warner:
"Core Data manages its caches intelligently so that if the results are updated by another call, the cache is removed if affected."

Related

How to remove only entities saved to store from Core Data?

I'm trying to build a caching system for a feed reading application. The idea is each time a new feed is successfully pulled, remove all stored entities in Core Data, and store the first twenty items of the feed (this is used as an offline cache).
The issue I'm running into is my managed object context may have hundreds of items in it when a pull to refresh is performed. I'd like to keep those items in the context while removing any stored items from Core Data and then store the twenty items returned from the refresh call.
For what it's worth, I'm using Magical Record. I've tried looking around for this solution, but either I'm using the wrong keywords or the information is hard to find.
I'm not sure what code to show exactly, but here's the handling of the feed call:
for (id dict in feedArray){
WFeedItem *item = [WFeedItem feedItemWithAttributes:[dict dictionaryByReplacingNullsWithBlanks] inManagedObjectContext:[NSManagedObjectContext defaultContext]];
[parsedArray addObject:item];
}
This gets passed back from the subclassed HTTPClient it's defined in to a view controller that has called it. Bear in mind, this all works fine, it's all a matter of deleting stored items while retaining everything I've gathered during this session in the context.
Just use a different context for importing and storing the new records. Your original object context can remain as it is.

When to call obtainPermanentIDsForObjects:?

I'm currently having an issue where creating a new object on a background child thread (whose parent is the main UI thread context) and saving causes my NSFetchedResultsController to show two new objects: one with a temporary objectID, and one with a permanent objectID. This seems to be a bug of some sort, unless I'm missing something.
So I thought I would manually obtain permanent IDs for any new objects I create. This fixes the duplicate row issue, but introduces new random errors (such as "could not fulfill fault for object", refering to the new object I created). If anyone has any ideas as to why any of the previously mentioned is happening, please share.
I'm guessing obtainPermanentIDs is a step in the right direction. But when do I call this method? Before saving to the child context? After saving the child and before the parent? After the parent?
Currently my setup is this:
masterMOC - private queue tied to the persistent store, so physical saves happen here
----mainMOC - main queue tied to the UI, child of masterMOC
-------backgroundMOC - private queue, child of mainMOC
So if I create a new object on backgroundMOC, and I intend to immediatly save to disk (which means I'll have to call save: on all three contexts), where should I be calling obtainPermanentIDs?
(or if anyone has a different solution other than calling obtain permanent ids? What problem was this method introduced to solve anyway? Why would I want to call this method?)
Update:
I think I figured out what's going on (it's only a theory though), though not how to solve it. Core Data apparently generates permanent IDs for objects when they are saved physically to disk. So in my case, this won't happen until I call save on the masterMOC. Currently what I do when creating a new object on the backgroundMOC is:
save on backgroundMOC (so that changes are pushed up one level to the mainMOC and the my table view can insert the new rows)
save on mainMOC (so that I can prepare for saving to disk)
save on masterMOC (which finally saves to disk)
What's happening here is that calling save on the backgroundMOC triggers a UI update, and causes the fetched results controller to insert a new object that still has only a temporary ID. But then calling save on masterMOC causes all objects to get assigned permanent IDs, which causes another UI update, inserting another row for this "new" object! By commenting out the last masterMOC save, I no longer see duplicate entries. Am I doing something wrong here, or is this some kind of bug?
Another update: I think I've pretty much confirmed the bug. I call save on the backgroundMOC and then set up a timer to call save on the mainMOC and masterMOC 5 seconds later. Immediatley upon saving to the backgroundMOC, a new row is inserted into my table. 5 seconds later (upon saving main and master), another new row is inserted. (the row inserted first has a temp id, and the newest insert has permanent id).
I had the exact same issue, of course after a particularly difficult and dispiriting day of debugging everything to find out the issue was temporary IDs. :)
I have the exact same structure as you, and I also have subclasses of NSManagedObjectContext to codify the behavior I expect of saves in the background and main contexts – namely, a save in the background context should save the main context (and the main context should sync any objects that changed with the external service, which is irrelevant but worth mentioning as an explanation for why I have two subclasses), and a save in the main context should save the master context.
In my RFSImportContext subclass (equivalent to your backgroundMOC), I implement - save: to call [super save:], then call [self.parentContext performBlock:] (self.parentContext here is equivalent to your mainM)C, where the block calls obtainPermanentIDsForObjects: with the contents of the main context's - updatedObjects and - insertedObjects arrays, then I save the main context.
I no longer have the leaking of temporary objects into my NSFetchedResultsController as you describe. A way to improve the situation a bit would be to use the RFSMainContext subclass (again, equivalent to your mainMOC) to implement - save: to obtain permanent object IDs, save itself, then save the master context. This codifies the behavior that we always want the main context to have permanent IDs for objects in it when it is saved.

FetchedResultsController and UITableView in multiple views

Summary: I have two different views which use tables to show results from a fetched results controller. These tables may contain the same data. I get errors when moving the rows in one table after the other table has been loaded. (Obvious I suppose!)
To simplify, imagine an entity consisting of countries, and another entity grouping these countries together into sets of countries. We have an "editSet" view which allows you to name the set, add or delete countries in the set, as well as reorder them, all using the standard UI. We then have a "viewSet" view that shows you these countries and some value associated with them (eg. exchange rate or whatever).
Now, in the edit view, when we reorder and moveRowAtIndexPath is invoked, I set a BOOL which stops any further UI changes until the coredata is updated (each record has a displayOrder integer which I update before doing another performSearch). This all works perfectly if you only have instantiated the "editSet" view.
Where things goes wrong is if you load "viewSet" then load "editSet" with the same set and move the rows. The BOOL we set in editSet doesn't get passed along to viewSet (which is also "watching" the coredata) and gets upset when the coredata is changed programmatically. This generates:
Serious application error. An exception was caught from the delegate of
NSFetchedResultsController during a call to -controllerDidChangeContent:
. *** -[NSMutableArray removeObjectAtIndex:]:
index 0 beyond bounds for empty array with userInfo (null)
And all hell breaks lose.
On the other hand, if I load/appear "viewSet" with A DIFFERENT SET to the one I am editing, no problems.
So, what I need to do is EITHER "disconnect" the FRC and the table when leaving viewSet (perhaps do a search on nothing and reload the table?) OR pass a BOOL to the viewSetController when saving the moved rows in coredata in editSet to mimc what I do locally in that viewcontroller (not quite sure how to do this but doable I guess).
I am sure I am not the first to come across this problem, so wondering, what's the best way?
Use another managed object context for your "edits" and then merge them back in when you go back to your "viewSet". Take a look in the Apple sample code project 'CoreDataBooks' to see how you can use two contexts to perform disjoint edits.

Limit NSFetchedResultsController results, and get more

HI All,
I currently have an NSFetchedResultsController setup to return all rows in a table in my core data database. This then fills up my UITableView. The trouble is this will quickly get out of hand as the rows grow in number.
How can I limit the initial query to say 20 results, then add a button somewhere to "Get More" from where we left off?
Thanks for any guidance as always
This is controlled with NSFetchRequest's -setFetchLimit: and -setFetchOffSet.
If I recall correctly, the drawback with NSFetchedResultsController is that you can't modify the fetch request after you create your NSFetchedResultsController instance. I believe this means you'll have to create a new one (instance w/new fetch request) each time you change the range you want to retrieve/display.
File an enhancement request with Apple at bugreporter.apple.com if you feel this shouldn't be the case.
To change the limit number on the fly you simply need to:
Access the fetchRequest of your NSFetchedResultsController instance, change the limit, delete the old cache if there is any and perform a new fetch.
Code:
[yourFetchedResultsController.fetchRequest setFetchLimit:50];
[NSFetchedResultsController deleteCacheWithName:"you cache name"];
[yourFetchedResultsController performFetch:nil];
fetchBatchSize only affects how many objects are fetched at a time. It will not limit number of objects in-memory concurrently so it is still possible to run out of memory. It is possible to limit the total concurrent objects with a combination of batchSize, fetchLimit, and offset but it requires deleting the cache or storing separate caches per "page", which seems un-ideal to me.
Another more hacky method to get around it is to re-create the NSFetchedResultsController, the results from the old controller will be faulted if possible, and you can start with a clean slate. Really crude, but it avoids deleting the cache.
I believe that instead of setting -setFetchLimit and limiting your NSFetchRequest (for new rows you have to create a new reqeust), set -fetchBatchSize to only control how many rows will be loaded into memory. Say, If you show 10 cells per view, set your batch size to double or so. As you scroll your view, the controller will automatically load new set into memory.

One NSFetchedResultsController vs several UITableViews — only one UITableView gets updates?

Setup:
I have several UITableViewControllers (tvc) that all instantiate a separate NSFetchedResultsController (frc). All these frc-s, though, use the same ManagedObjectContext (moc).
Scenario:
Sometimes, from one of the tvc-s, I launch some background process to update data. The background process updates the data in a separate thread and finally calls [moc mergeChangesFromContextDidSaveNotification:] on the main thread. These changes should then propagate to frc, and [frcDelegate controllerDidChangeContent:] should be called. This works fine... but only for the frcDelegate whose associated tvc initiated the update. (all the tvc-s are acting as frcDelegate for their frc.)
Expected behavior:
since all the frc-s are bound to the same moc, all the frc-s should pick up the changes, and [frcDelegate controllerDidChangeContent:] should be called for all of them.
Actual behavior:
only one [frcDelegate controllerDidChangeContent:] gets called.
Why am I seeing actual behavior instead of expected? How should I debug this? Or is this the correct behavior?
This is the expected behavior.
Each fetched results controller will only observe changes in the entity that is specified in its fetch request. Presumably you are only modifying objects of the entity that corresponds to the controller that initiated the update.