views:

2051

answers:

1

I'm working on modifying some existing heavy-handed code that simply calls [tableView reloadData] on any change, to using more specific table updates with the insert/delete methods.

However, I'm getting some really bad behavior in doing so. Previously, as one would imagine, when the table loaded, it only requested cells for the rows that were visible at the time. This was the behavior when reloadData was used.

Now that insertSections is being called, all cells are requested after that update, which can be hundreds. This results in cells being created for every row, completely ruining the reusable cell queue and just being all around wasteful. I must be doing something wrong.

The change is this simple, code that results in the tableView asking only for visible rows:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    // ... ensure it's the right key
    [tableView reloadData];
}

Code that results in the tableView asking for everything:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    // ... ensure it's the right key
    NSUInteger sectionCount = [self sectionCount];
    NSIndexSet *indices = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)];
    [tableView insertSections:indices withRowAnimation:UITableViewRowAnimationFade];
}

I can toggle back and forth to see the behavior change. Frustrating. Ideas?


Adding a bounty just to see if anyone has any more insight.

The beginUpdates/endUpdates doesn't affect anything, and I wouldn't expect it to, this is just one command, there's nothing extra to coalesce into a single update.

I'm thinking this is simply a side effect of desiring the animation. To have everything "slide" in, it has to have everything to render. Game over.

+3  A: 

It aappars you are telling the table to insert all of the sections, which is basically equivalent to a reload. When you tell the table it needs to load a section it needs to read all the items in that section. Also, you are doing it outside of a beginUpdates/endUpdates block, which means every time you add in the section the table has to immediately commit the changes. If you wrap everything inside of a beginUpdates/endUpdates it will suspend all the queries until it is done which will allow the system to coalesce redundant queries and eliminate queries that turn out not to be necessary.

You should only be calling insertSections with new sections that are added. If you do that then tableview does not have to query the information of all the unaltered sections, even if they move around. Also, if elements inside an section change but the section itself doesn't you should use the row version of the methods to modify those specific rows inside of the section, and only those rows will get queried.

Below is an example (from a CoreData based app) that should get the point of across. Since you have a custom model instead an NSFetchedResultsController you will have to figure out the type of action going on instead of just checking a bunch of constants handed to you by the system.

//Calculate what needs to be changed     

[self.tableView beginUpdates];

for (i = 0 i < changeCount; i++) {
  type = changes[i]; 

  switch(type) {
    case NSFetchedResultsChangeInsert:
      [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                    withRowAnimation:UITableViewRowAnimationFade];
      break;

    case NSFetchedResultsChangeDelete:
      [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                    withRowAnimation:UITableViewRowAnimationFade];
      break;
  }
}

switch(type) {

  case NSFetchedResultsChangeInsert:
    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                          withRowAnimation:UITableViewRowAnimationFade];
    break;

  case NSFetchedResultsChangeDelete:
    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                          withRowAnimation:UITableViewRowAnimationFade];
    break;

  case NSFetchedResultsChangeUpdate:
    [self configureCell:(id)[tableView cellForRowAtIndexPath:indexPath]
            atIndexPath:indexPath];
    break;

  case NSFetchedResultsChangeMove:
    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                          withRowAnimation:UITableViewRowAnimationFade];
    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section]
                  withRowAnimation:UITableViewRowAnimationFade];
    break;
  }
}

[self.tableView endUpdates];
Louis Gerbarg
Well, at little extra information may be useful, this is a completely empty table initially. I deferred loading of the data to keep the UI responsive, so it comes up blank initially and adds the sections as they come in off the network. So going from no sections to all sections using reloadData works fine, but explicitly adding sections results in chaos? Weird.
Nick Veys
No, that is expected, and why you need the beginUpdates/endUpdates calls wrapping what you are doing. Without those the tableView has to assume that after every modification you do will may impact the display, which means it has to query all the sections to calculate what cells are visible, and then get all the that will be visible. When you do a beginUpdates the tableView stops updating until you call endUpdates, at which point it can do it all a single time with querying all your intermediary states.
Louis Gerbarg
"without querying your intermediary states," I need more coffe
Louis Gerbarg