Update
I have posted my solution to this problem as an answer below. It takes a different approach from my first revision.
Ori
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.
So finally here is my solution to this issue. This method can be applied to tables of any size, any number of sections (as far as I can tell)
As before I have modified Matt Gallagher's tableview Code which places cell-specific logic in a separate cell controller. However, you can easily adapt this method to a different model
I have added the following (relevant) ivars to Matt's code:
NSArray *allTableGroups; //always has a copy of every cell controller, even if filtered
NSArray *filteredTableGroups; //always has a copy of the filtered table groups
Matt's original ivar:
NSArray *allTableGroups
…always points to one of the above arrays.
This can probably be refactored and improved significantly, but I haven't had the need. Also, if you use Core Data, NSFetchedResultsController makes this easier.
Now on to the method (I am trying to comment as much as I can):
- (void)createFilteredTableGroups{
//Checking for the usual suspects. all which may through an exception
if(model==nil)
return;
if(tableGroups==nil)
return;
if([tableGroups count]==0)
return;
//lets make a new array to work with
NSMutableArray *newTableGroups = [[allTableGroups mutableCopy] autorelease];
//telling the table what we are about to do
[self.tableView beginUpdates];
//array to track cells for deletion animation
NSMutableArray *indexesToRemove = [NSMutableArray array];
//loop through each section
for(NSMutableArray *eachSection in tableGroups){
//keeping track of the indexes to delete for each section
NSMutableIndexSet *indexesForSection = [NSMutableIndexSet indexSet];
[indexesForSection removeAllIndexes];
//increment though cell indexes
int rowIndex = 0;
//loop through each cellController in the section
for(ScheduleCellController *eachCellController in eachSection){
//Ah ha! A little magic. the cell controller must know if it should be displayed.
//This you must calculate in your business logic
if(![eachCellController shouldDisplay]){
//add non-displayed cell indexes
[indexesForSection addIndex:rowIndex];
}
rowIndex++;
}
//adding each array of section indexes, EVEN if it is empty (no indexes to delete)
[indexesToRemove addObject:indexesForSection];
}
//Now we remove cell controllers in newTableGroups and cells from the table
//Also, each subarray of newTableGroups is mutable as well
if([indexesToRemove count]>0){
int sectionIndex = 0;
for(NSMutableIndexSet *eachSectionIndexes in indexesToRemove){
//Now you know why we stuck the indexes into individual arrays, easy array method
[[newTableGroups objectAtIndex:sectionIndex] removeObjectsAtIndexes:eachSectionIndexes];
//tracking which cell indexPaths to remove for each section
NSMutableArray *indexPathsToRemove = [NSMutableArray array];
int numberOfIndexes = [eachSectionIndexes count];
//create array of indexPaths to remove
NSUInteger index = [eachSectionIndexes firstIndex];
for(int i = 0; i< numberOfIndexes; i++){
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:sectionIndex];
[indexPathsToRemove addObject:indexPath];
index = [eachSectionIndexes indexGreaterThanIndex:index];
}
//delete the rows for this section
[self.tableView deleteRowsAtIndexPaths:indexPathsToRemove withRowAnimation:UITableViewRowAnimationTop];
//next section please
sectionIndex++;
}
}
//now we figure out if we need to remove any sections
NSMutableIndexSet *sectionsToRemove = [NSMutableIndexSet indexSet];
[sectionsToRemove removeAllIndexes];
int sectionsIndex = 0;
for(NSArray *eachSection in newTableGroups){
//checking for empty sections
if([eachSection count]==0)
[sectionsToRemove addIndex:sectionsIndex];
sectionsIndex++;
}
//updating the table groups
[newTableGroups removeObjectsAtIndexes:sectionsToRemove];
//removing the empty sections
[self.tableView deleteSections:sectionsToRemove withRowAnimation:UITableViewRowAnimationTop];
//updating filteredTableGroups to the newTableGroups we just created
self.filteredTableGroups = newTableGroups;
//pointing tableGroups at the filteredGroups
tableGroups = filteredTableGroups;
//invokes the animation
[self.tableView endUpdates];
}
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 suspect that you are forgetting to remove the object representing the section from your internal storage, so that the -numberOfSectionsInTableView:
method is still returning 1 after all sections are deleted.
That's exactly what I was doing wrong when I had the same crash!
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.
A much simpler way to address this is to update your data source, then call reloadSections
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
This will reload a single section. Alternatively you could use indexSetWithIndexesInRange:
to reload multiple sections simultaneously.