I have an async worker-thread that communicate with a backend and, if needed, create new objects in my CoreData model, or simply make changes on existing objects if needed to reflect the "server-truth", especially an attribute (integer) that I call status
.
Every time such a change to an object is done, it's triggered via NSNotificationCenter to all UI subscribers, among them a UITableViewController that hold a nice list of all objects, using sections to group the objects by their status
. Hence, all objects with status 0 are placed in one section, all objects with status 1 is placed in another section, etc...
Since it's an async worker-thread, I've setup my CoreData-stack to have a main-context connected to the PSC, whereas each individual async thread has it's own private context where changes are merged up to the main context and then stored persistently.
Problem
The async worker-thread that detect a change when communicating with the backend, will after updating the CoreData-model via it's private context, run the postNotification code
[[NSNotificationCenter defaultCenter] postNotificationName:@"ObjectChanged"
object:self
userInfo:userInfo];
... which in turn results in all registered subscribers begin triggered by the same async thread. This makes things a little strange since UI-stuff must be invoked in main-thread, and my UI (holding the table view with sections for each status) would, when the postNotification is triggered, like to delete/insert some cells in the tableView to reflect the detected changes.
But, since I cannot directly just inject the UI-changes, I prepare the added and deleted NSIndexPath
s in a dictionary that is sent to be executed in the main-thread;
NSMutableArray* addedIndexPaths = [[NSMutableArray alloc] init];
NSMutableArray* removedIndexPaths = [[NSMutableArray alloc] init];
//... based on some logic, remove and/or add
[addedIndexPaths addObject:[NSIndexPath indexPathForRow:xxx inSection:yyy];
...
[removedIndexPaths addObject:[NSIndexPath indexPathForRow:xxx inSection:yyy];
...
//Anything to animate...?
if (addedIndexPaths.count > 0 || removedIndexPaths.count > 0)
{
NSDictionary* animParams = [NSDictionary dictionaryWithObjectsAndKeys:
addedIndexPaths, @"added",
removedIndexPaths, @"removed",
nil];
[self performSelectorOnMainThread:@selector(updateTableViewWithChanges:) withObject:animParams waitUntilDone:YES];
}
Which finally take us to the updateTableViewWithChanges
function, being run by the main-thread;
- (void)updateTableViewWithChanges:(NSDictionary *)animParams
{
NSArray* addedIndexPaths = [animParams objectForKey:@"added"];
NSArray* removedIndexPaths = [animParams objectForKey:@"removed"];
//Now, finally -- let's update UI!
[self.tableView beginUpdates];
if ([addedIndexPaths count] > 0)
[self.tableView insertRowsAtIndexPaths:addedIndexPaths withRowAnimation:UITableViewRowAnimationTop];
if ([removedIndexPaths count] > 0)
[self.tableView deleteRowsAtIndexPaths:removedIndexPaths withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
}
All is good, most of the time, but then once in a while the app crashes due to inconsistency during the table update;
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
I'm suspecting that this is due to the main-thread deciding to update it's UI while my "did-detect-changes" code is running, resulting in an UI-state that already IS up-2-date, which imply that when I'm calling the dynamic code of "adding another one at NSIndexPath xxx", this does not add-up.
Do you have any alternative approach to achieving what I need to do? How else would you go about and implement a async-thread updating state that you want to be reflected in the UI while still running the worker-thread and incrementally step-by-step update the UI? Am I barking up the wrong tree entirely, or just missing something along the way?
Looking forward to your thoughts and ideas, and if you're still reading -- thank you ;)
Edit (show code for how I fetch data from CoreData, since I suspect timing to the the root pif this crash)..
The private context held by the async worked thread is merged to main contact and then saved in accordance with recommendations (using performBlockAndWait
), but perhaps the actual save done by the worker-thread just before notifying subscribers that things has changed, is taking time (even though I'm using performBlockAndWait
, and not performBlock
), and that when the main context is used to fetch the object for rendering, it's not consistent?
Here's code for when I read/fetch an object (by its unique GUID that is just an attribute);
NSDictionary *substitutionDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
guid, @"GUID", nil];
NSFetchRequest *fetchRequest = [self.managedObjectModel fetchRequestFromTemplateWithName:@"FindObjectByGuid" substitutionVariables:substitutionDictionary];
//Run the fetch!
NSError *error = nil;
//Uses proper context depending on if it's a worker thread or the main context/UI
NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
if (error == nil)
{
//Great! We did it!
NSLog(@"Great! Success fetched %lu object(s) from PS mathing given GUID", (unsigned long)[results count]);
if ([results count] > 0)
object = [results objectAtIndex:0];
}
/Markus