the doc about reloadData of UITableView says(http://developer.apple.com/library/ios/#documentation/uikit/reference/UITableView_Class/Reference/Reference.html): "it should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates".
My question is, why? (especially first part, italic).
There's a book which I am reading which implements adding items to the UITableView like this:
// Add new item to the table
- (IBAction)addNewItem:(id)sender
{
// Update the model, add a new item
BNRItem *newItem = [[BNRItemStore sharedStore] createItem];
// Figure out where that item is in the array
int lastRow = [[[BNRItemStore sharedStore] allItems] indexOfObject:newItem];
// Create the corresponding index path
NSIndexPath *ip = [NSIndexPath indexPathForRow:lastRow inSection:0];
// Insert this new row into the table
[[self tableView] insertRowsAtIndexPaths:[NSArray arrayWithObject:ip]
withRowAnimation:UITableViewRowAnimationTop];
}
whereas same could be achieved also like this:
// Add new item to the table
- (IBAction)addNewItem:(id)sender
{
// Update the model, add a new item
[[BNRItemStore sharedStore] createItem];
[[self tableView] reloadData];
}
By reload data you're forcing all rows to be recreated so when you have information what is new it is just much more performant to tell table view what have to be added/removed, especially when, for example, you added one row at the end and that part is currently not on screen (so view does not have to be re-rendered, only scroll high have to be recalculated).
Related
I have 3 or 2 sections (depending on datasource), in my grouped UITableView. I am trying to reload the last section via:
dispatch_async(dispatch_get_main_queue(), ^{
[UIView performWithoutAnimation:^{
[feedDetailTB reloadSections:[NSIndexSet indexSetWithIndex:feedDetailTB.numberOfSections-1] withRowAnimation:UITableViewRowAnimationNone];
}];
});
First of all, the footer never disappears. The data source basically keeps track of whether there are more comments or not (a simple load more functionality). In the viewForFooterInSection I simply return nil, when all the comments have been loaded.
But, as you see in the GIF, at first the loading button stays there. It is even accessible and works. When I scroll up, it vanishes and one can see it in the bottom, which is correct. But after all the comments have been reloaded, it should vanish, but sadly it stays there.
If I use reloadData it works fine. But I can't used it, since I have other sections, which I don't need to reload.
Second, there is a weird animation/flickering of the row items, even when I have used UITableViewRowAnimationNone. Not visible in the GIF
You should implement "isTheLastSection" according to your logic
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (isTheLastSection) {
return 40;
}
return 0;
}
In order to add new rows to a section, you must use the insertRowsAtIndexPaths rather than just adding new objects to data source and reloading a section.
Here's the code:
NSMutableArray *newCommentsIndexPath = [[NSMutableArray alloc] init];
for (NSInteger i = currentCount; i < (_postDetailDatasource.commentsFeedInfo.allCommentsArray.count + serverComments.count); i ++)
{
NSIndexPath *idxPath = [NSIndexPath indexPathForRow:i inSection:sectionNumber];
[newCommentsIndexPath addObject:idxPath];
}
[_postDetailDatasource.commentsFeedInfo.allCommentsArray addObjectsFromArray:serverComments];
[feedDetailTB beginUpdates];
[feedDetailTB insertRowsAtIndexPaths:newCommentsIndexPath withRowAnimation:UITableViewRowAnimationFade];
[feedDetailTB endUpdates];
Thanks in advance. I'm dealing with a single UITableView which is divided into different sections. Users can add or delete cells only from specific sections. The information displayed in the UITableView is stored in an array where each object represents a section:
eg: tableItemsArray = ((arrayForSection1), (arrayForSection2), (arrayForSection3))
I have the animation set up to handle the delete as follows:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationRight];
I then programmatically remove the item from the array to remove it from the data model. However I am having difficulty in how to insert the cell in an animated form.
Currently I am running the current pseudo-algorithm:
Check item isn't anywhere in the array
Add item to array into a specific section
sort this section of the array alphabetically
reload the data
Update
When a new item is added to the array it is stored in a string to keep record of the new item. Once the array has been resorted I iterate through the array of cells to find its indexPath, then I delete the string (to prevent recursive insertion animations) and perform the animation, however the code doesn't animate its insertion into the table. I wondered whether that maybe the animation code (as below) wasn't working as I was using [tableView reloadData]?
// For Each Section in the table:
for (NSMutableArray *object in sectionsInTable) {
// For Each Cell in a section (Cells are stored in an array in index 1)
for (NSString *label in [object objectAtIndex:1]) {
if ([label isEqualToString:_insertedObject]) {
// New item
_insertedObject = Nil;
// animate addition
[self.labelListTableView beginUpdates];
[self.labelListTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:[[object objectAtIndex:1] indexOfObject:label] inSection:[sectionsInTable indexOfObject:object]]] withRowAnimation:UITableViewRowAnimationBottom];
[self.labelListTableView endUpdates];
}
}
}
Any ideas why the animation isn't working?
Thanks
D
Seems that the [tableView reloadData] was preventing the animation.
// Annimate the new Item
// For Each Section in the table:
for (NSMutableArray *object in sectionsInTable) {
// For Each Cell in a section:
for (NSString *cell in [object objectAtIndex:1]) {
if ([cell isEqualToString:_insertedObject]) {
// New item
_insertedObject = Nil;
// animate addition
[tableView beginUpdates];
NSInteger section = [sectionsInTable indexOfObjectIdenticalTo:object];
NSInteger row = [[object objectAtIndex:1] indexOfObjectIdenticalTo:cell];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
[tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationBottom ];
[tableView endUpdates];
}
}
}
I've a tableView in a UIViewController. When I load the viewController, is loading the tableView with 3 cells. Every time I make the scroll on my tableView and finish at the bottom of it, I wish that were added to it (other) 3 cells. Just how does the facebook app! Help me please! This is what I experienced in my code (the cells are not added):
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
//I created an int flag because this method (as soon as it initialize the
//table) is called 2 times (because when it load the viewController, is called
// the method that populates the arrays that contain the details to be shown in
// the cells and reload the tableView)
if (_flagCells == 0 || _flagCells == 1) {
_flagCells ++;
return;
}
if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
//phpClass is a class that contain scripts used to connect my app to script php for mySQL DB
//objsRequest is a method of phpClass that receive a query for input and return an array of //results of that query. cellsLimit represent the cells to be shown (Once every 3 to 3) and //numberOfDequeuedCells represent the number of cells to be added (every 3) that is SELECT .. //FROM .. LIMIT 0,3 .... LIMIT 3,3 .... LIMIT 6,3 ....
[_arrID addObjectsFromArray:[_phpClass objsRequest:[NSString stringWithFormat:#"SELECT ID FROM mii LIMIT %d,%d",_cellsLimit,_numberOfDequeuedCells]]];
for (int i = (int)[_arrID count]-3; i < [_arrID count]; i++) {
//objRequest is a method of phpClass that receive a query for input and return a string that
//represent the result of that query
[_arrNames addObject:[_phpClass objRequest:[NSString stringWithFormat:#"SELECT Name FROM mii WHERE ID = %#",_arrID[i]]]];
[_arrGen addObject:[_script objRequest:[NSString stringWithFormat:#"SELECT Gen FROM mii WHERE ID = %#",_arrID[i]]]];
//getImg is a method of phpClass that receive the user ID for input and return an image
//representing the user image
UIImage *img = [_phpClass getImg:_arrID[i]];
[_arrImgs insertObject:img atIndex:i];
//Here is the problem because the rows doesn't be added to my tableView
[_tableView beginUpdates];
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
}
_cellsLimit +=3;
[_tableView endUpdates];
}
}
I did that myself some time ago. If I remember right, then I just loaded additional data into the Array that I used as container for the data in the table. Once the data was loaded then I redraw the table. That's it.
I think you are getting lost just because you try to do it more complicated.
As for your comment:
[_tableView performSelector:#selector(reloadData) withObject:nil afterDelay:0.001];
or
[_tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
both should execute the reloadData in another thread and therefore should not cause a loop. However, even then you should add the new data just before the end of the table is reached.
I am trying to delete rows in an array without having to use the edit function that Apple provide (Something along the lines of -(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath). So the user selects some of the rows, presses a button not in the table view, then those rows fade away. Apple provide something of sorts here.
The code I am using calls an array of rows to be deleted, which is defined elsewhere, removes the array population objects at those particular rows, and is supposed to remove all of the rows using the fade stuff.
- (void) deleteTableRow {
NSIndexPath *current;
NSLog(#"to be deleted: %#", toBeDeleted); //toBeDeleted is the array with the NSIndexPath items
for(int i=0; i < [toBeDeleted count]; i++) {
current = [toBeDeleted objectAtIndex: i];
[tableData removeObjectAtIndex:current.row]; //Remove the necessary array stuff
}
tv = [UITableView alloc];
[tv beginUpdates];
[tv deleteRowsAtIndexPaths:toBeDeleted withRowAnimation:UITableViewRowAnimationFade];
[tv endUpdates];
// Do whatever data deletion you need to do...
}
tv is defined in my header file and is a referencing outlet of my UITableView.
So here are my main questions:
Because my UITableView is not a UITableViewController (it is part of the view instead), is this even possible?
If it is possible, why is this not working?
I want to use iOS 5's nifty row-movement calls to animate a tableview to match some model state changes, instead of the older-style delete-and-insert.
Changes may include both reordering and in-place updates, and I want to animate both, so some rows will need reloadRowsAtIndexPaths.
But! UITableView appears to be just plain wrong in its handling of row reloads in the presence of moves, if the updated cell shifts position because of the moves. Using the older delete+insert calls, in a way that should be equivalent, works fine.
Here's some code; I apologize for the verbosity but it does compile and run. The meat is in the doMoves: method. Exposition below.
#define THISWORKS
#implementation ScrambledList // extends UITableViewController
{
NSMutableArray *model;
}
- (void)viewDidLoad
{
[super viewDidLoad];
model = [NSMutableArray arrayWithObjects:
#"zero",
#"one",
#"two",
#"three",
#"four",
nil];
[self.navigationItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:
#ifdef THISWORKS
#"\U0001F603"
#else
#"\U0001F4A9"
#endif
style:UIBarButtonItemStylePlain
target:self
action:#selector(doMoves:)]];
}
-(IBAction)doMoves:(id)sender
{
int fromrow = 4, torow = 0, changedrow = 2; // 2 = its "before" position, just like the docs say.
// some model changes happen...
[model replaceObjectAtIndex:changedrow
withObject:[[model objectAtIndex:changedrow] stringByAppendingString:#"\u2032"]];
id tmp = [model objectAtIndex:fromrow];
[model removeObjectAtIndex:fromrow];
[model insertObject:tmp atIndex:torow];
// then we tell the table view what they were
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:changedrow inSection:0]]
withRowAnimation:UITableViewRowAnimationRight]; // again, index for the "before" state; the tableview should figure out it really wants row 3 when the time comes
#ifdef THISWORKS
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:fromrow inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:torow inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
#else // but this doesn't
[self.tableView moveRowAtIndexPath:[NSIndexPath indexPathForRow:fromrow inSection:0]
toIndexPath:[NSIndexPath indexPathForRow:torow inSection:0]];
#endif
[self.tableView endUpdates];
}
#pragma mark - Table view data source boilerplate, not very interesting
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return model.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#""];
if (cell == nil)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#""];
[cell.textLabel setText:[[model objectAtIndex:indexPath.row] description]];
[cell.detailTextLabel setText:[NSString stringWithFormat:#"this cell was provided for row %d", indexPath.row]];
return cell;
}
What the code does: sets up a tiny model (small mutable array); when a button is pushed, it makes a small change to the middle element of the list, and moves the last element to be the first. Then it updates the table view to reflect these changes: reloads the middle row, removes the last row and inserts a new row zero.
This works. In fact, adding logging to cellForRowAtIndexPath shows that although I ask for row 2 to be reloaded, the tableview correctly asks for row 3 because of the insert once it's time to actually do the update. Huzzah!
Now comment out the top #ifdef to use the moveRowAtIndexPath call instead.
Now the tableview removes row 2, asks for a fresh row 2 (wrong!), and inserts it in the final row-2 position (also wrong!). Net result is that row 1 moved down two slots instead of one, and scrolling it offscreen to force a reload shows how it's gone out of sync with the model. I could understand if moveRowAtIndexPath changed the tableview's private model in a different order, requiring the use of the "new" instead of "old" index paths in reloads or model fetches, but that's not what's going on. Note that in the second "after" pic, the third and fourth rows are in the opposite order, which should't happen no matter which cell I'm reloading.
My vocabulary has grown colorful cursing Apple. Should I be cursing myself instead? Are row moves just plain incompatible with row reloads in the same updates block (as well as, I suspect, inserts and deletes)? Can anyone enlighten me before I go file the bug report?
I just spent some time playing with your code, and I agree; looks like it just doesn't work.
This whole area is a bit under-documented, but they don't actually say that you can mix moveRowAtIndexPath:toIndexPath: with reload methods. It does say in the that it can be mixed with row-insertion and row-deletion methods. Those seems to work if I modify your code to exercise those instead. So, you might be asking for an enhancement, not filing a bug. Either way, I'd definitely send it to radar.