iPhone SDK: Inserting and updating a UITableView with a new row - iphone

I have a tableView that needs to be updated after information has been inserted from another view. If I perform a
[self.tableView reloadData];
The very next time I insert more information in another view and try to reload the table, all the currently visible rows are duplicated.
In other words, when I start up the app I have:
tableView:
Row 1
Row 2
Then I submit some information that will also show up in the table and suddenly I have:
tableView
Row 1
Row 2
Row 3 <- info I just added
Row 1
Row 2
My numberOfRowsInSection implementation looks like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [ItemsController sharedItemsController].count;
}
My cellForRowAtIndexPath implementation looks like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ItemsController* controller = [ItemsController sharedItemsController];
NSMutableArray* recentItems = controller.listOfRecentItems;
CustomCell *cell = nil;
NSUInteger row = [indexPath row];
if( row < recentItems.count )
{
Items* item = [recentItems objectAtIndex:row];
if( recentCellData == nil )
recentCellData = [[NSMutableDictionary alloc] initWithCapacity:[indexPath length]];
if( [recentCellData count] > 0 )
cell = [recentCellData objectForKey:[NSString stringWithFormat:#"%d", row]];
if (cell == nil) {
UIViewController * view1 = [[UIViewController alloc] initWithNibName:#"CustomCell" bundle:nil];
cell = (CustomCell*)[view1 view];
[recentCellData setObject:cell forKey:[NSString stringWithFormat:#"%d",row]];
}
// do some other stuff here
}
// Set up the cell
return cell;
}
What's the best way to update the table and avoid duplicating the currently visible rows.
Thank in advance for all the help!

The error isn't in how you're reloading the table, it's in how you're providing data to it. Set a breakpoint in the data source methods and the method that adds new rows to see where you're going wrong.

You'll only end up with five items if tableView:numberOfRowsinSection: returns 5. Thats the simple answer to your question, but I see other problems here. I'm wondering why you have this test: row < recentItems.count. Is that array the same thing as [ItemsController sharedItemsController].count? You really need to be using the same array for both methods.
(Also, it's not a syntax error, but you shouldn't use the property syntax for things that aren't declared as properties. You should write [recentItems count] instead.)
I'm also confused by the code you use to set up the cell. Cells are meant to be reusable. That is, you create one cell, then reconfigure it every time in your implementation of tableView:cellForRowAtIndexPath:. Your code creates a cell for each item in your list. This is very memory-inefficient, and will likely crash your program due to insufficient memory on the iPhone if you keep lots of cells in memory like this.
The recommended approach is to call dequeueReusableCellWithIdentifier:. If that returns nil, then you set up a cell using the initWithFrame:reuseIdentifier: initializer. The table view is very smart, and will only ask you to redraw the cell when it needs you to.
Your recentCellData dictionary looks really shaky to me, too. What if you insert an item after the item with key #"2"? All the items with key #"3" onward will need to be shifted one element to the right to work the way you expect. That's a ton of bookkeeping that seems rather unnecessary to me. If you really needed something like this -- and to be clear, I don't think you do -- why wouldn't you use an NSMutableArray, which is much easier to use?

I added a bit more info above.

Related

Populating UITableView row 0 static and the rest with NSArray

In one section of my app I have a UITableView which is working fine right now. I would like to set row 0 cell.textLabel.text to #"Some string". Once row 0 has been set I would then like to load the rest of the rows from an array. Currently on load my array populates the table view but I'm trying to set row 0 as a sticky. The closest example I can think of is a forum topic that is set to stay at the top. My array is constructed of returned data from a web service call.
It's been a while since I've messed with table views, and I'm having a blank on this one.
The table view is 1 section, and I get the rows by counting the elements in the array. Since I would like to create an additional cell (row 0) I would call [array count] + 1. I don't know if this approach is the best one which is why I'm reaching out to the community here.
Any insight or a shove in the right direction would be great at this point.
You're on the right track:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [array count]+1;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if ([indexPath row] == 0) {
// Code for first
[[cell textLabel] setText:#"First cell"];
} else {
[[cell textLabel] setText:[array objectAtIndex:[indexPath row]-1]];
}
return cell;
}
If you want the top of your table to be "sticky", why not consider using that string as a section header or title? In this case, the header stays visible at all times until the next section (e.g. if you had two sections, that is) is fully on the screen.
In any event, in one of my current projects I'm required to do roughly the same thing that you're doing and I have a static string being returned in row 0 (which scrolls off the top of screen when the table view scrolls down).
And in my UITableViewDataSource method, I always add one for the static cell to the number of objects in my array and in my "cellForRowAtIndexPath:" method, I increment the row by one when the indexPath.row is not zero. And if it is zero, I return my static string.
And dark_knight provides some nice sample code that illustrates what I was describing to you. So +1 to him/her.

Scrolling UITableview Application going to be crash?

In my application I am using UITableview. In that TableView I am displaying an array that contains dictionaries. The last row of the TableView contains one button which indicates user load more events. If the user clicks on the button we need to get data from a service and load the data in the UITableview.
For that purpose I am trying to implement it like the code below:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section**
{
if (ifMore)
{
return [totalArray count];
}
else
{
return [tableArray count];
}
return 0;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
int count =[tableArray count];
if (ifMore)
{
count= [totalArray count];
}
if (indexPath.row==count-1)
{
return 96;
}
return 56;
}
The cell for row index method is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
TableCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell = [[TableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
NSMutableDictionary *dict=[[NSMutableDictionary alloc]init];
if (ifMore)
{
dict=[totalArray objectAtIndex:indexPath.row];
}
else
{
dict =[tableArray objectAtIndex:indexPath.row];
}
cell.hostLabel.text=[dict objectForKey:#"Event_Name"];
cell.startTime.text=[dict objectForKey:#"Event_Startdate"];
cell.endTime.text= [dict objectForKey:#"Event_Enddate"];
cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;
int count=[tableArray count];
if(ifMore)
{
count=[totalArray count];
}
if (indexPath.row==count-1)
{
[cell.contentView addSubview:moreBtn];
}
return cell;
}
I am using the code like this. Now my main problem is when I am loading data normally it is good does not make any issue while loading scrolling. Its works perfectly. But when I am clicking on the button in the TableView cell and get data from service and reload data. It works fine. But when I am scrolling it crashes. I am stuck with this.
The crash message is:
2012-12-20 12:22:49.312 IAGDapp[3215:15e03] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 4 beyond bounds [0 .. 3]'
*** First throw call stack:
(0x4b5052 0x21c7d0a 0x4a1db8 0x2b9e6 0x2b7d2 0xc80a57 0xc80b92 0xc8669e 0x7402db 0x7401af 0x489966 0x489407 0x3ec7c0 0x3ebdb4 0x3ebccb 0x1f6b879 0x1f6b93e 0xc2ba9b 0x28e0 0x2105 0x1)
terminate called throwing an exceptionsharedlibrary apply-load-rules all
kill
Can you help me find the error(s)?
Here when you click on UIButton of cell after here you get another data from services and after you want to load this data in same table so here follow this step..
first when you get data then add this data in your tableArray and totalArray also.
after that reload table.
Its crash because here numberOfRows and other methods are not found this new updated array.
I agree with Paras on this one. I can't see the part where you fetch the data into arrays but if you omit properly reloading data/ getting the data from the wrong array/ setting ifMore variable, your app will crash. First you should check them. If you think nothing is wrong, set a breakpoint inside numberOfRowsInSection and see why it returns 4 where you want to have 4+ cells.
But actually, I dont think you need two different arrays and ifMore variable for that purpose. Have a single array, update it after you fetch the data, reload the table and remove totalArray and all the if statements with ifMore variable. Simplicity may help with stability.
The problem is your loop.
if (indexPath.row==count-1)
{
return 96;
}
Count of your array is 4 and you are trying to load 96 rows in your tableview. Hence, your application is getting crash.
Problem is not scrolling UITableView. Problem is with loading the array in table.
Seems like ifMore variable is not working as you were expecting. Since the error log was index 4 beyond bounds [0 .. 3], it is clear that, the tableview's datasource contains 3 elements only, but the count you returned in numberOfRowsInSection were 4.
As an alternate solution, if you want your More button always as last row in your datasource, why cant you do it using single array. Just return the tableArray.count + 1(for more button) in numberOfRowsInSection method. In cellForRowAtIndexPath, check whether if the index was above tableArray.count, then create button. Same as in heightForRowAtIndex method too.

Displaying a "No rows found" message in UITableView with Core Data

I have implemented an iPhone app that uses UITableViewController/UITableView and Core Data. Further, I use a NSFetchedResultsController to manage the table data. This was all very straight forward and works great. I then decided that I should display a message in the UITableView when no rows where found/retrieved. After researching this, it appeared that the best way (perhaps the only way) to do this was to return a "dummy" cell that contains the message. However, when I do this, I get a nastygram from the runtime system that complains (and rightfully so) about data inconsistencies: "Invalid update: invalid number of sections. The number of sections contained in the table view ...". Here is the relevant code:
- (NSInteger) numberOfSectionsInTableView: (UITableView *)tableView
{
if ([[self.fetchedResultsController fetchedObjects] count] == 0) return 1;
return [[self.fetchedResultsController sections] count];
}
- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if ([[self.fetchedResultsController fetchedObjects] count] == 0) return 1;
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex: section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *) tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
if ([[self.fetchedResultsController fetchedObjects] count] == 0) {
UITableViewCell *cell = [[UITableViewCell alloc] init];
cell.textLabel.text = #"No widgets found.";
return cell;
}
STCellView *cell = (STCellView *)[tableView dequeueReusableCellWithIdentifier: #"ShieldCell"];
[self configureCell: cell atIndexPath: indexPath];
return cell;
}
I have read responses from similar questions and it appears that I should use
insertRowsAtIndexPaths: withRowAnimation:
to insert the "dummy" message row into my table. However, this also means removing the "dummy" row when a real row is inserted. I can do this, but it seems like there should be an easier way to accomplish this. All I want to do, is to display a message indicating that there are no rows in the table (simple enough?). So, my question is this: Is there a way to display a message in an UITableView without using the "dummy" cell approach OR is there a way to convince UITableViewController/NSFetchResulsController that this is only a "dummy" row and they should not get so upset about it because it is not a real row (from my point of view) in the table?
Any help you can provide would be very appreciated (I am a struggling newbie to iPhone development and I want to learn the best practices). Thanks.
Rather than hack with the tableview datasource to get the intended UI you should add the "No rows found" message to the tableview header instead.
I did as follows in viewDidLoad.
UILabel *label = [[UILabel alloc] init];
[label setTextColor:[UIColor lightGrayColor]];
[label setText:#"No widgets found."];
[label sizeToFit];
label.frame = CGRectMake((self.tableView.bounds.size.width - label.bounds.size.width) / 2.0f,
(self.tableView.rowHeight - label.bounds.size.height) / 2.0f,
label.bounds.size.width,
label.bounds.size.height);
[self.tableView insertSubview:label atIndex:0];
In this case, each TableViewCells must be opaque to hide the label. or need to toggle the hidden property of the label according to the row count.
An alternative approach, which I have used before is to use Core Data to manage the update for you by inserting a 'no rows' entity for the section where no rows have been detected in your model class, which handles the data update.
There are a number of ways to implement this e.g. set the name/title field to a known status message or a flag within the entity. Once inserted you can detect the 'no rows' entity in the cellForRowAtIndexPath delegate method and insert an alternative table cell to show the message.
Just remove the 'no rows' entity before refreshing the data for that section.
My simple suggestion to display an empty message is to rearrange your controller to be a simple UIViewController (not a UITableViewController).
This UIViewController is composed by a UITableView (the controller is the data source and the delegate for your table) and by a UILabel (or a UIView that contains a UILabel) that displays the empty row message.
In this manner you can control the visibility of the table and the label based on the retrieved rows.
This approach could be laborious but I think it's good to avoid hacking NSFetchResultsController and data source. Furthermore you could have a complete control on arranging the position for your empty message.
As #Rog suggested you could also use the table view header to display that message. As you prefer.
Hope it helps.

When an UITableView is empty, show an UIImage

This is related to another question of mine which wasn't answered in a helpful way (message when a UITableView is empty).
I'm trying to show an UIImage graphic that says You haven't saved any bookmarks over an UITableView when it's empty. I have NSNotification set-up so that when bookmarks are added or deleted, a message is sent so that the UITableView can be updated.
I've been trying to do it with this code. Why won't this work?
- (void)bookmarksChanged:(NSNotification*)notification
{
[self.tableView reloadData];
UIImageView* emptyBookmarks = [[UIImageView alloc] initWithFrame:CGRectMake(75, 100, 160, 57)];
emptyBookmarks.alpha = 1;
emptyBookmarks.image = [UIImage imageNamed:#"emptyBookmark.png"];
[self.view addSubview:emptyBookmarks];
[emptyBookmarks release];
if ([self.dataModel bookmarksCount] == 0)
{
emptyBookmarks.alpha = 1;
}
else
{
emptyBookmarks.alpha = 0;
}
}
I'm probably approaching this the wrong way... But if salvageable, what am I doing wrong?
When I initially have an empty bookmarks tableview, there's no image displayed. After I add a bookmark and then delete it, the image shows. Grrh.
Another way (and IMO the correct way) to do this is to manipulate the backgroundView property on the UITableView.
While making a single cell with a custom image cell would certainly works, I think it overly complicates the logic of your UITableViewController's data source. It feels like a kludge.
According to UITableView documentation:
A table view’s background view is automatically resized to match the
size of the table view. This view is placed as a subview of the table
view behind all cells , header views, and footer views.
Assigning an opaque view to this property obscures the background color
set on the table view itself.
While you probably don't want to just set it to your UIImageView, it is very easy to make a UIView that contains the UIImageView that you want.
Well first off if you were going to do it that way, you would need to reload the tableView after updating the image or model etc. and not before.
But you are probably making things more complicated than they need to be!
Why not just check to see if the data for section 0 and indexPath.row 0 are empty and if so in cellForRowAtIndexPath display a text message accordingly.
// First make sure there is always one row returned even if the dataModel is empty.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger numRows = 0;
if ([self.dataModel lastObject]) {
// Return the number of rows in the section.
numRows = [self.dataModel count]; // etc.
}
if (numRows < 1) numRows = 1;
return numRows;
}
// Then display the data if there is some, otherwise a message if empty.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
if ([self.dataModel lastObject]) {
// setup the cell the normal way here.
} else { // the datasource is empty - print a message
cell.textLabel.text = nil;
cell.detailTextLabel.text = NSLocalizedString(#"You haven't saved any bookmarks", #"");
cell.detailTextLabel.textColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.7];
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
Are you sure [self.dataModel bookmarksCount] is equal to 0 ?
While I agree that you are probably going about this the wrong way,
your image is allocated and added in your bookmark changed, your notification does not trigger when there are no bookmarks initially. Hence you don't see the image. Call the bookmar changed when your table view inits or appears.
Probably the best way to achieve this is to perform a check in your numberOfRowsInSection method to return 1 if your data source is empty. Then in cellForRowAtIndexPath check if your data source is empty and if it is, create a custom cell that contains whatever you want. In heightForRowAtIndexPath you need to return your custom cell height if your datasource is empty, but only if you want the cell larger than the default. At least that is how I would approach it.
when bookmarks count is nil add one to your row method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
int c;
c = bookmarks.count;
if(c == 0){
c = 1;
}
return c;
}
and then the same check again in your cellforrowatindexpath.
Another thing to be aware of in this situation is that if you're using core data and you're datasource is feeding off an entity, you will want to make sure your model matches. You can get some weird side-effect behavior in certain situations. This is especially true if you allow editing and core data has an empty model but you're tableview is still showing a cell.

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!