0

in my app I have a UITableView containing messages. Each row is one message. When I delete a message, I first delete it from the messages array and then use deleteRowsAtIndexPaths:

    int index =  (int)[self.messages indexOfObject:message];
    [self.messages removeObject:message];

    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
    NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];

    [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];

This works fine however when a new message is received, the push notification triggers the table refresh. Sometimes a new message will be placed in the array just before deleteRowsAtIndexPaths finishes executing and hence the app crashes because the number of rows in the table after the method completes does not equal the number of rows before minus the number of deleted rows. Example error message:

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 (3) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

Has anyone come across a problem like this before? Is there another approach or workaround I can take to stop the app crashing like this?

Any pointers would be great

iAnurag
  • 9,286
  • 3
  • 31
  • 48
Kex
  • 8,023
  • 9
  • 56
  • 129
  • 1
    You need to make sure that `cellForRowAtIndexPath:`, along with the other delegate/data source methods, are all in sync. After deleting a row manually, the row count returned by your data source should be one less. Also, protect access to your underlying array. If your app is multithreaded, then either use synchronized blocks or GCD to ensure that nothing is modifying your underlying structure in the background while the main/foreground thread is accessing it. – Craig Otis Jun 30 '15 at 14:03
  • so would a better approach be to use GCD and put the deleteRowsAtIndexPaths: and reloadData methods in a queue so it doesn't crash? That way if the app is still processing the deleteRowsAtIndexPaths the reloadData will wait until it completes first. – Kex Jun 30 '15 at 14:19
  • Yes - but instead of putting them in a queue, it is probably easier and more sensible to simply always perform those operations on the main queue. Often those GUI-touching methods will only function (or only function _properly_) on the main/UI thread. If you have a long-running background method that retrieves data, just have it `dispatch_async` out to the main thread when it finishes with the updated values. – Craig Otis Jun 30 '15 at 14:23
  • Something like this? http://stackoverflow.com/questions/14128759/fifo-serial-queue-using-gcd would it be okay to put [self.tableView reloadData] in one of the dispatch_async blocks? Do these all run on the main thread like you mentioned? – Kex Jun 30 '15 at 14:27

1 Answers1

1

I had the same kind of issue. And what i did:

  1. declare activity flag for you update:

    @property (nonatomic, assign) BOOL canEdit;

  2. insert self.canEdit = NO when your perform some update operation

  3. override willDisplayCell and check, if there will display last cell of your tableview's datasource array (so, update will end after that cell)

    -(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath  
    { 
    
    if ([dataArray count]- 1 == indexPath.row && self.canEdit == NO)  
                    self.canEdit = YES;}
    
  4. When you receive push notification - check this flag, and if it's YES - perform update. If no - request update after some time

Doro
  • 2,413
  • 2
  • 14
  • 26