Race conditions and Advanced iOS Architecture in UITableViewController
Yesterday I attended the fantastic Functional Swift conference in Brooklyn, and had a chance to ask a question of one of the presenters, Andy Matuschak, who until recently worked for Apple on UIKit and gave the great 2014 WWDC talk, Session 229, "Advanced iOS Application Architecture and Patterns."
So I was able to ask about something that's been bugging me (haha) about UITableView since seeing his WWDC talk. (It's not core functional programming issue, more about dangers of shadowy, unseen caches.) After looking it up today, I see the issue isn't in the API, it's in the provided code included automatically in a new project for Master-Detail in a UITableViewController. It's the prewritten UITableViewDelgate method called in response to user interaction managed by the table:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { [self.objects removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; } else if (editingStyle == UITableViewCellEditingStyleInsert) { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view. } }
Notably, under "if (editingStyle == UITableViewCellEditingStyleDelete)" there are 2 lines of code that
Deletes the item from the data source, an NSMutableArray self.objects.
Tells the tableview to delete the row with an animation.
Here we have the perfect example identified in Matuschak's WWDC talk: Code fired in response to the user interaction updates the model, and separately alters the UI.
Experience with UITableView makes clear why this example is not nitpicking or hypothetical: After the animation, the tableview will immediately call its datasource, (ignoring sections) including numberOfRowsInSection:
If the implementation of this data source method gets its answers from the ultimite data source, if for any reason that data source hasn't finished updating the data source deletion, there will be an invalid number of rows exception.
Worse, if the delay to update the data source is variable and only sometimes fails to update by the time numberOfRowsInSection: is called, the bug could only happen sometimes and be hard to reproduce.
In my GTD to-do app that uses core data, I'm using a fetchedResultsController In my own code, following the plan of the WWDC talk, I've reduced the commitEditingStyle: (which only handles delete) to only fire off a line of code to the managedObjectContext to delete the object given by the self.fetched results controller, and then, in the fetchedResultsControllerDelegate, have the change made.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the row from the data source [self.managedObjectContext deleteObject:[self.fetchedResultsControllerForProjects objectAtIndexPath:indexPath]]; } } -(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath switch(type) { case NSFetchedResultsChangeDelete: { [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; } ...
So he truth resides in Core Data, and the UI won't be updated until the change has been made.
Obviously, the provided code for UITableViewController is boilerplate, prewritten code for demonstration purposes. And deleteObjectAtIndex has no BOOL return. Still, this is the kind of pattern of UI making two calls, one for animation and one to update the data source, that will lead to unwieldy and hard-to-debug programs.
Edit: I have since noticed that this is in fact the prewritten code on UITableViews if you check "Use Core Data," although I had been working with Collection Views and had to suss it out myself; how you implement this on Collection Views, which lack beginUpdates: and endUpdates:, is another post.










