UITableView: deleting sections with animation

Viewed 58264

Update

I have posted my solution to this problem as an answer below. It takes a different approach from my first revision.


Original Question I previously asked a question on SO that I thought solved my issues:

How to deal with non-visible rows during row deletion. (UITableViews)

However, I now have similar problems again when removing sections from a UITableView. (they resurfaced when I varied the number of sections/rows in the table).

Before I lose you because of the shear length of my post, let me state the problem clearly, and you can read as much as you require to provide an answer.


Problem:

If batch deleting rows AND sections from a UITableView, the application crashes, sometimes. It depends on the configuration of the table and the combination of rows and sections I choose to remove.

The log says I crashed because it says I have not updated the datasource and the table properly:

Invalid update: invalid number of rows in section 5.  The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted).

Now quickly, before you write the obvious answer, I assure you I have indeed added and deleted the rows and sections properly from the dataSource. The explanation is lengthy, but you will find it below, following the method.

So with that, if you are still interested…


Method that handles removal of sections and rows:

- (void)createFilteredTableGroups{

    //index set to hold sections to remove for deletion animation
    NSMutableIndexSet *sectionsToDelete = [NSMutableIndexSet indexSet];
    [sectionsToDelete removeIndex:0];


    //array to track cells for deletion animation
    NSMutableArray *cellsToDelete = [NSMutableArray array];

    //array to track controllers to delete from presentation model
    NSMutableArray *controllersToDelete = [NSMutableArray array];

    //for each section
    for(NSUInteger i=0; i<[tableGroups count];i++){

        NSMutableArray *section = [tableGroups objectAtIndex:i];

        //controllers to remove
        NSMutableIndexSet *controllersToDeleteInCurrentSection = [NSMutableIndexSet indexSet];
        [controllersToDeleteInCurrentSection removeIndex:0];
        NSUInteger indexOfController = 0;

        //for each cell controller
        for(ScheduleCellController *cellController in section){

            //bool indicating whether the cell controller's cell should be removed
            NSString *shouldDisplayString = (NSString*)[[cellController model] objectForKey:@"filteredDataSet"];
            BOOL shouldDisplay = [shouldDisplayString boolValue];

            //if it should be removed
            if(!shouldDisplay){

                NSIndexPath *cellPath = [self indexPathOfCellWithCellController:cellController]; 

                //if cell is on screen, mark for animated deletion
                if(cellPath!=nil)
                    [cellsToDelete addObject:cellPath];

                //marking controller for deleting from presentation model
                [controllersToDeleteInCurrentSection addIndex:indexOfController];                

            }
            indexOfController++;
        }

        //if removing all items in section, add section to removed in animation
        if([controllersToDeleteInCurrentSection count]==[section count])
            [sectionsToDelete addIndex:i];

        [controllersToDelete addObject:controllersToDeleteInCurrentSection];

    }


    //copy the unfiltered data so we can remove the data that we want to filter out
    NSMutableArray *newHeaders = [tableHeaders mutableCopy];
    NSMutableArray *newTableGroups = [[allTableGroups mutableCopy] autorelease];


    //removing controllers
    int i = 0;
    for(NSMutableArray *section in newTableGroups){
        NSIndexSet *indexesToDelete = [controllersToDelete objectAtIndex:i];
        [section removeObjectsAtIndexes:indexesToDelete];
        i++;
    }

    //removing empty sections and cooresponding headers
    [newHeaders removeObjectsAtIndexes:sectionsToDelete];
    [newTableGroups removeObjectsAtIndexes:sectionsToDelete];

    //update headers
    [tableHeaders release];
    tableHeaders = newHeaders;

    //storing filtered table groups
    self.filteredTableGroups = newTableGroups;


    //filtering animation and presentation model update
    [self.tableView beginUpdates];
    tableGroups = self.filteredTableGroups;
    [self.tableView deleteSections:sectionsToDelete withRowAnimation:UITableViewRowAnimationTop];
    [self.tableView deleteRowsAtIndexPaths:cellsToDelete withRowAnimation:UITableViewRowAnimationTop];
    [self.tableView endUpdates];


    //marking table as filtered
    self.tableIsFiltered = YES; 


}

My guess:

The problem seems to be this: If you look above where I list the number of cells in each section, you will see that section 5 appears to increase by 1. However, this is not true. The original section 5 has actually been deleted and another section has taken its place (specifically, it is old section 10).

So why does the table view seem not to realize this? It should KNOW that I removed the old section and should not expect a new section that is now located at the old section's index to be bound by the deleted section's number of rows.

Hopefully this makes sense, it is a little complicate to write this out.

(note this code worked before with a different number of rows/sections. this particular configuration seems to give it issues)

7 Answers

I’ve run into this problem before. You are trying to delete all rows from a section and then, in addition, that now empty section. However, it is sufficient (and proper) to remove that section only. All rows within it will be removed as well. Here is some sample code from my project that handles deletion of one row. It needs to determine whether it should remove only this row from a section or delete the entire section if it is the last remaining row in that section:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        // modelForSection is a custom model object that holds items for this section.
        [modelForSection removeItem:[self itemForRowAtIndexPath:indexPath]];

        [tableView beginUpdates];

        // Either delete some rows within a section (leaving at least one) or the entire section.
        if ([modelForSection.items count] > 0)
        {
            // Section is not yet empty, so delete only the current row.
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
        }
        else
        {
            // Section is now completely empty, so delete the entire section.
            [tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] 
                     withRowAnimation:UITableViewRowAnimationFade];
        }

        [tableView endUpdates];
    }
}

I notice that you're deleting the sections from the table first, and then deleting rows.

I know there's a complicated discussion of batch insertion and deletion for UITableViews in the Table View Programming Guide, but it doesn't specifically cover this.

I think what's happening is that deleting the sections is causing the row deletions to refer to the wrong row.

i.e. you want to delete section #2 and row #1 from section #4... but after you've deleted section #2, the old section #4 is now the third section, so you when you delete with the old NSIndexPath of (4, 1) you're deleting some random different row that may not exist.

So I think the fix might be as simple as swapping those two lines of code, so you're deleting the rows first, then the sections.

I saw this same exact error as the result of prematurely releasing the background view of my custom tableview cell.

With NSZombieEnabled I got a an exception being thrown way down below an internal call to a function to prepare the cell for reuse. Without NSZombieEnabled, I was getting the Internal consistency error.

Incidentally when I fixed the retain/release issue on the background view of the cell, I was able to delete the last row of the section without having to delete the section explicitly.

Moral of the story: This error just means something bad is happening when you try to delete, and one of the things that happens when you delete is the cell gets prepared for reuse, so if you are doing anything custom with your tableview cells, look for a possible error there.

Related