NSZombie - should retain/release calls be paired by library? - iphone

TL:DR version: I used NSZombieEnabled to find source of EXC_BAD_ACCESS error and saw a library has 1 more release than retains. Can I assume that this library is causing the crash or can that release be associated with a retain from another library?
I am having some problems in my app with a UITableViewCell subclass instance getting messages after its retain count reaches 0. I ran the app with NSZombies and am currently trying to pair the retain/release calls to find where exactly is the error originating from. I noticed that there are only 2 retains and 3 releases with "Responsible Library" set to QuartzCore. Does it mean that that extra release call is the one that causes my app to crash? Or is it possible that a release has associated retain in another library?
Additional info:
My section headers are tappable, when one is selected, the row for this section is inserted into table view and any previously visible row is deleted. In other words, there can be only 1 section at a time that has 1 row, all other sections must have 0 rows.
The release/retain calls from QuartzCore that I paired are:
CALayer layoutSublayers (retains)
CA::Layer::layout_if_needed(CA::Transaction*) (releases)
The release without a pair is:
CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)
The exact line where I get the crash is the endUpdates line in:
- (void)sectionHeaderView:(SectionHeaderView *)sectionHeaderView sectionOpened:(NSInteger)sectionOpened {
SectionInfo *sectionInfo = [self.sectionInfoArray objectAtIndex:sectionOpened];
sectionInfo.open = YES;
NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];
[indexPathsToInsert addObject:[NSIndexPath indexPathForRow:0 inSection:sectionOpened]];
/*
Create an array containing the index paths of the rows to delete: These correspond to the rows for each quotation in the previously-open section, if there was one.
*/
NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
NSInteger previousOpenSectionIndex = self.openSectionIndex;
if (previousOpenSectionIndex != NSNotFound) {
SectionInfo *previousOpenSection = [self.sectionInfoArray objectAtIndex:previousOpenSectionIndex];
previousOpenSection.open = NO;
previousOpenSection.category.model = nil;
[previousOpenSection.headerView toggleOpenWithUserAction:NO];
[indexPathsToDelete addObject:[NSIndexPath indexPathForRow:0 inSection:previousOpenSectionIndex]];
}
// Style the animation so that there's a smooth flow in either direction.
UITableViewRowAnimation insertAnimation;
UITableViewRowAnimation deleteAnimation;
if (previousOpenSectionIndex == NSNotFound || sectionOpened < previousOpenSectionIndex) {
insertAnimation = UITableViewRowAnimationTop;
deleteAnimation = UITableViewRowAnimationBottom;
}
else {
insertAnimation = UITableViewRowAnimationBottom;
deleteAnimation = UITableViewRowAnimationTop;
}
NSIndexPath *indexToDelete = [indexPathsToDelete firstObject], *indexToInsert = [indexPathsToInsert firstObject];
if (indexToDelete == nil) {
NSLog(#"no row to delete");
}
else {
NSLog(#"deleting row %d section %d", [indexToDelete row], [indexToDelete section]);
}
NSLog(#"inserting row %d section %d", [indexToInsert row], [indexToInsert section]);
// Apply the updates.
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:insertAnimation];
[self.tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:deleteAnimation];
[self.tableView endUpdates]; // this is the crash.
self.openSectionIndex = sectionOpened;
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:sectionOpened] atScrollPosition:UITableViewScrollPositionNone animated:YES];
}
The error happens on iOS7.

You can get a EXC_BAD_ACCESS at the endUpdates line if the index paths in the arrays are invalid (e.g. a section number of -1). You should NSLog your indexPathsToInsert and indexPathsToDelete and make sure they have valid values in them.

I have already found a fix to this problem. It seems that one of the subviews of the table cell was calling becomeFirstResponder after an asynchronous request has ended. Of course if the cell was freed already by that moment, the crash happened. I just assumed at first that the problem is because of unbalance in retains and releases but looks like that wasn't it.
And to answer the question in title: When running with NSZombieEnabled Instruments app already pair some retain/releases when it can assume they are connected. On one of the runs of my app, Instruments joined paired that release coming from QuartzCore with a retain coming from UIKit. Even though I can't be 100% sure that it's right, I assume that "Responsible library" tags being identical is not obligatory.
TL:DR - I'm pretty sure that retain can be paired with release coming from another library.

Related

UITextField artifacts

I have an iPhone app which has a Table View-based data input screen with a toggle, which when on shows all rows in another section of the table.
Sometimes, when the app is first loaded, and usually when it has been fully deleted from the phone, the UITextFields from the original section of the table are displayed in addition to the new rows, as below (main table section is above)
:
THe strangest thing about this is that this behaviour only occurs the first time this screen is displayed - it seems fine after that. Oh, and it only seems to occur on the phone, not the simulator.
Given the random nature of this, could it have something to do with other apps? I did have apps with similar namespaces running at the same time. The problem seemed to go away after I closed the apps down / deleted from phone.
I have included the code block that is run when the switch is changed below:
- (void)accountSwitchChanged {
[customerDetails saveField:#"addToAccount" WithBool:addToAccountSwitch.on];
NSArray *indexes = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:1 inSection:1], [NSIndexPath indexPathForRow:2 inSection:1], [NSIndexPath indexPathForRow:3 inSection:1],nil];
if (addToAccountSwitch.on)
[detailsTable insertRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationFade];
else
[detailsTable deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationFade];
}
Any ideas?
--EDIT
cellForRowAtIndexPath code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// get rid of grey background for iPad app
[tableView setBackgroundView:nil];
UITableViewCell *cell = nil;
NSDictionary *cellData = [[dataSourceArray objectAtIndex: indexPath.section] objectAtIndex:indexPath.row];
id cellControl = [cellData objectForKey:kCellControlKey];
static NSString *kCellControl = #"CellControl";
cell = [tableView dequeueReusableCellWithIdentifier:kCellControl];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellControl] autorelease];
cell.textLabel.textColor = [[[ConfigManager sharedInstance].skin valueForKey:#"tableCellHeadingTextColour"] toUIColor];
cell.backgroundColor = [[[ConfigManager sharedInstance].skin valueForKey:#"tableCellBgColour"] toUIColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:17];
} else {
// a cell is being recycled, remove the old control (if it contains one of our tagged edit fields)
UIView *viewToCheck = nil;
viewToCheck = [cell.contentView viewWithTag:kDetailsViewTag];
if (!viewToCheck)
[viewToCheck removeFromSuperview];
}
// if control is not a text field, make it a button
if (![cellControl isKindOfClass:[UITextField class]])
cell.selectionStyle = UITableViewCellSelectionStyleGray;
else
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = [cellData objectForKey:kCellTitleKey];
[cell.contentView addSubview:cellControl];
return cell;
}
Something to check and eliminate If it is only occurring the first time view is displayed, it's always worth checking whether you are assuming the view is loaded before you are doing work on it. This doesn't come up very often, but when it does it can be very hard to track down. To check whether this might be your issue, add a [viewController view] call just after you create the view for the first time. This forces the view to be loaded. If the issue goes away when you add this, you've tracked the source of the issue. And you can even leave the [viewController view] call in as a fix, or work through your code to allow for lazy instantiation.
All this said, far more likely to be something funny in your tableView:cellForRowAtIndexPath: code. If you want good stack overflow, post all or relevant fragment of the that code (or the relevant code it calls).
Follow up on posted code:
(1) if (!viewToCheck) [viewToCheck removeFromSuperview] won't do anything. It'll only send a removeFromSuperview message when viewToCheck is nil and that won't do anything. Not sure if this will be key issue, but it's something to put right.
(2) [cell.contentView addSubview:cellControl]; - this looks problematic - it's usually best to only add subviews when you create the cell. You can hide or show in the main body of tableView:cellForRowAtIndexPath: - that would, I suspect, work for you here. With this code there's a risk here that you'll either add multiple subviews when you only want one or (which may be what's going on here) that you end up not removing a subview when a cell is recycled. If the viewToCheck code is supposed to be removing the added cellControl then, as (1), it won't do that just now because your if condition is wrong.

ReloadTable crashes app

This code causes the app to crash! I'm looping through a dictionary and everything logs out fine. It works when i write -1 in the for loop but not otherwise,
-(void)updateProjectTimes {
for(int i = 0; i < [projectsTable numberOfRowsInSection:0]-1; i++) {
NSString *currentValue = [[[projects objectForKey:#"activeProjects"] objectAtIndex:i] objectForKey:#"timepassed"];
NSString *finishValue = [[[projects objectForKey:#"activeProjects"] objectAtIndex:i] objectForKey:#"timeleft"];
if([currentValue intValue] < [finishValue intValue]) {
[[[projects objectForKey:#"activeProjects"] objectAtIndex:i] setObject:[NSString stringWithFormat:#"%d", ([currentValue intValue] + 1)] forKey:#"timepassed"];
} else {
[(NSMutableArray *)[projects objectForKey:#"activeProjects"] removeObjectAtIndex:i];
if([[projects objectForKey:#"activeProjects"] count] == 0) {
[projectTimer invalidate];
projectTimer = nil;
}
}
//[projectsTable reloadData]; works if i put it here!
}
[projectsTable reloadData]; //<- But not here!! :(
}
I assume you are wokring with tableView. Apparently you are modifing the data source of your tableView. When you remove an object from the data source, you have to adjust your tableView as well. Meaning either call reloadData, or call [tableView deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation].
The reason your app doesn't crash if you put -1, could be -- perhaps -- because there is only on item that matches the [currentValue intValue] < [finishValue intValue] condition, so that if you go through [projectsTable numberOfRowsInSection:0]-1, after removing that object, numberOfRowsInSection matches the count of projectsTable.
But it is good only for one cycle. When the next cycle happens, in the if...loop, you app crashes again, unless you include the [projectsTable reloadData] in the same if...loop.
While the reloadData method works just fine, but if you are simply removing a row, or adding a row to your table by adding or removing objects, its better to use deleteRowsAtIndexPaths or insertRowsAtIndexPaths methods. There will be less overhead and work for your app, and would make it smoother and faster.
The bottom line, to make your code work, right after [(NSMutableArray *)[projects objectForKey:#"activeProjects"] removeObjectAtIndex:i]; remove the corresponding object from your tableView by calling deleteRowsAtIndexPaths.
Alternatively, you can also use beginUpdates and endUpdates. For a complete reference, refer to http://developer.apple.com/library/ios/#documentation/uikit/reference/UITableView_Class/Reference/Reference.html
Hope it helps.

How to prevent counting an empty NSMutableArray

I would like to simulate the SMS Bubbles of the iPhone for my own app. I found some nice code overhere (FYI): http://vimeo.com/8718829 . It is a restyled UITableView. Exactly what a wanted.
Here is the problem:
- The Tableview is filled with an array of messages
- It needs to be a NSMutable array because you want to add messages on the fly.
- When there are no messages yet, the message-array is empty.
- But counting an empty NSMutableArray causes an exeception, the app crashes. (you need the count for scrolling).
So what is a nice solution for that? I now pre fill the array with "". But that is very ugly. You see a mini bubble on the screen.
Can you hide cells? In the example on the video, there are already 2 messages so the problem does no occur.
Any suggestion is welcome. Tnx
Christian
Actually counting an empty array does not raise any exception. I think the problem is here:
- (void)add {
if(![field.text isEqualToString:#""])
{
[messages addObject:field.text];
[tbl reloadData];
NSUInteger index = [messages count] - 1;
[tbl scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
field.text = #"";
}
}
As the "-1" index cannot exist. You can edit that line to
NSUInteger index = MAX(0, [messages count] - 1);
And it should work.
You can count an empty array (I assume you mean [arrayName count]) as long as its alloc'd so make sure its initialized somewhere earlier.

Reorder-able UITableView with Max # of Rows per Section

I'm new to iPhone dev and programming in general, having recently completed Stephen Kochan's Programming in Objective-C 2.0, and parts of Erica Sadun's iPhone Cookbook. Just reading answered questions on this site has helped me a lot, so I owe a big thanks to all its users. Now I'm working on my first iPhone app, and am making good progress, save for one thing that's got me stumped.
I'm having some trouble with a UITableView. Basically, I have a table that needs to have a maximum of 6 rows per section, with no minimum (except if the row of a 1-row section is deleted, in which case the section goes along with it). The table can also be reordered by the user. When the user drags a row into a section that already has the maximum 6 allotted rows, I want the bottom row of that section to sort-of 'budge' down to become the top row of the next section.
As for implementing this, my first thought was to call a method in tableView:moveRowAtIndexPath:toIndexPath: that would go through the sections in a loop, making sure that none of them had 7+ rows, adjusting the array as needed, and then using a [myTable beginUpdates] block to delete and insert the required rows. This is what all that looks like (note: my array structure is: Array (Table) > Arrays (Sections) > Dictionaries (Items)):
-(void) tableView: (UITableView *) tableView moveRowAtIndexPath: (NSIndexPath *) from toIndexPath: (NSIndexPath *) to
{
// Get the dictionary object for the icon.
NSMutableDictionary *theIcon = [[[TABLE_ARRAY_MACRO objectAtIndex: from.section] objectAtIndex: from.row] mutableCopy];
// Now remove it from the old position, and insert it in the new one.
[[TABLE_ARRAY_MACRO objectAtIndex: from.section] removeObjectAtIndex: from.row];
[[TABLE_ARRAY_MACRO objectAtIndex: to.section] insertObject: theIcon atIndex: to.row];
if ( [[TABLE_ARRAY_MACRO objectAtIndex: to.section] count] > 6 )
[self budgeRowsAtSection: to.section];
// Now we're done with the dictionary.
[theIcon release];
if ( PM_DEBUG_MODE )
NSLog(#"Pet moved!");
}
-(void) budgeRowsAtSection: (NSUInteger) section
{
if ( PM_DEBUG_MODE )
NSLog(#"Budging rows...");
// Set up an array to hold the index paths of the cells to move.
NSMutableArray *budgeFrom = [[NSMutableArray alloc] init];
NSMutableArray *budgeTo = [[NSMutableArray alloc] init];
// Start at the current section, and enumerate down to the nearest page with < 6 items.
int i = section;
while ( i < [TABLE_ARRAY_MACRO count] ) {
if ( PM_DEBUG_MODE )
NSLog(#"Attempting to budge rows in section %i!", i);
if ( [[TABLE_ARRAY_MACRO objectAtIndex: i] count] > 6 ) {
// Grab the last object, and move it to the beginning of the next array.
NSMutableDictionary *lastIcon = [[[PET_ICON_DATA objectAtIndex: i] lastObject] mutableCopy];
[[TABLE_ARRAY_MACRO objectAtIndex: i] removeLastObject];
[[TABLE_ARRAY_MACRO objectAtIndex: (i + 1)] insertObject: lastIcon atIndex: 0];
// Create an index path, and reflect the changes in the table.
[budgeFrom addObject: [NSIndexPath indexPathForRow: 6 inSection: i]];
[budgeTo addObject: [NSIndexPath indexPathForRow: 0 inSection: (i + 1)]];
// Now we're done with the icon.
[lastIcon release];
}
if ( PM_DEBUG_MODE )
NSLog(#"Rows budged for section %i!", i);
++i;
}
if ( PM_DEBUG_MODE )
NSLog(#"From cells: %#\nTo cells: %#", budgeFrom, budgeTo);
if ( PM_DEBUG_MODE )
NSLog(#"Data budged! Updating table...");
[editTable beginUpdates];
[editTable deleteRowsAtIndexPaths: budgeFrom withRowAnimation: UITableViewRowAnimationBottom];
[editTable insertRowsAtIndexPaths: budgeTo withRowAnimation: UITableViewRowAnimationTop];
[editTable endUpdates];
[budgeFrom release];
[budgeTo release];
if ( PM_DEBUG_MODE )
NSLog(#"Row budge done!");
}
The problem is when I run this, it invariably throws an exception; it gives me either Invalid update: number of rows in section after update must be equal to number of rows before update, + or - the number inserted or deleted (etc.), or Attempted to create two animations for cell, depending upon which tweak I'm trying. As for what those tweaks are, I've also tried using reloadSections:withRowAnimation:, and of course a simple [editTable reloadData]. I've tried placing the call to this method in the relevant TableView delegate/data source methods, including targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:, and the somewhat mysterious willMoveToRowAtIndexPath:fromIndexPath: method that is mentioned in the Apple guide on TableViews, but is non-existent elsewhere.
I've searched all over the Google, this site, and Apple's docs for any inkling of a solution, but to no avail.
So, to put my question simply, what is the best way to go about this? Is there some obvious thing that I'm overlooking? Or is my implementation concept just fundamentally flawed from the get-go?
Thanks in advance for the help! Any insight would be much appreciated.
-- Drew R. Hood
UPDATE: Following Jordan's advice from his comment on his answer, I've verified that the actual edits to the data source are going through correctly. I now consistently get the error Attempt to create two animations for cell when the table edit block executes (that timing verified by common sense and breakpoints). So I'm doing something wrong in my edit block. The current code for that is exactly as shown above. Ideas?
[editTable beginUpdates]; [editTable
deleteRowsAtIndexPaths: budgeFrom
withRowAnimation:
UITableViewRowAnimationBottom];
[editTable insertRowsAtIndexPaths:
budgeTo withRowAnimation:
UITableViewRowAnimationTop];
[editTable endUpdates];
I believe you need to update the DataSource before you actually delete the Row in whatever section you're deleting it from. So in this code, before you do the delete, delete the row from Table_Array_Macro (which I think is where your data is).
So, I've finally solved the problem and thought I'd post what I did here for the sake of posterity. It's quite simple, really: I just moved the actual insertion and deletion of rows in the table off to a separate method, which I then invoked after a delay. It had been clear from the start, and Jordan helped me to confirm, that the problem was occurring when performing the updates on the table itself, rather than in the process of actually changing data. So here's the code that did the trick:
-(void) performBudges
{
if ( PM_DEBUG_MODE )
NSLog(#"Performing budge animations...");
[editTable beginUpdates];
[editTable deleteRowsAtIndexPaths: budgeFrom withRowAnimation: UITableViewRowAnimationRight];
[editTable insertRowsAtIndexPaths: budgeTo withRowAnimation: UITableViewRowAnimationRight];
[editTable endUpdates];
[editTable performSelector: #selector(reloadData) withObject: nil afterDelay: 0.5];
[reloadedSections release];
[budgeFrom release];
[budgeTo release];
if ( PM_DEBUG_MODE )
NSLog(#"Budges completed!");
}
And all I have to do to perform this is add this snippet into my moveRowAtIndexPath: method:
if ( [[PET_ICON_DATA objectAtIndex: to.section] count] > 6 ) {
[self budgeRowsAtSection: to.section];
[self performSelector: #selector(performBudges) withObject: nil afterDelay: 1.0];
}
So I hope this helps anyone who may have this problem in the future. Thanks to Jordan and all who viewed this question. :)

UITableView: deleting sections with animation

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)
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.
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 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.
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!
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.
or just do this
- (void)tableView:(UITableView *)tv
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if(editingStyle == UITableViewCellEditingStyleDelete) {
//Delete the object from the table.
[directoriesOfFolder removeObjectAtIndex:indexPath.row];
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
}
directories of folder being your Array! Thats is all above codes didnt work for me! This is less expensive to do and just makes sense!