How to animate rows in UITableView on its loading? - ios

I want to use UITableViewRowAnimation with :reloadData.
Before i used that example for Table loading (I found that in some ios dev guide):
for (NSDictionary *entry in [dl reverseObjectEnumerator]) {
int insertIdx = 0;
[_allEntries insertObject:entry atIndex:insertIdx];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:insertIdx inSection:0]]
withRowAnimation:UITableViewRowAnimationRight];
}
But that way seems to be really slow on big data, so how to fast load tableviews with animation?

You should take a look at beginUpdates and endUpdates. Within these two commands you can update/remove/insert rows and have them animated for you. It's also much more efficient than using reloadData.
More on this topic here: http://developer.apple.com/library/ios/#documentation/uikit/reference/UITableView_Class/Reference/Reference.html
Quick excerpt:
(void)beginUpdates
Call this method if you want subsequent insertions, deletion, and
selection operations (for example, cellForRowAtIndexPath: and
indexPathsForVisibleRows) to be animated simultaneously. This group of
methods must conclude with an invocation of endUpdates. These method
pairs can be nested. If you do not make the insertion, deletion, and
selection calls inside this block, table attributes such as row count
might become invalid. You should not call reloadData within the group;
if you call this method within the group, you will need to perform any
animations yourself.

Wasabii is right, but in your case you could accumulate all indexes in array, then update table with one insert command:
NSMutableArray *indexes = [NSMutableArray arrayWithCapacity:5];
for (NSDictionary *entry in [dl reverseObjectEnumerator])
{
int insertIdx = 0;
[_allEntries insertObject:entry atIndex:insertIdx];
[indexes addObject:[NSIndexPath indexPathForRow:insertIdx inSection:0]];
}
// insert rows into table
[self.tableView insertRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationRight];

Related

Dynamically changing data source causing deleteRowsAtIndexPaths:indexes to crash

Tearing my hair out trying to get this to work. I want to perform [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];,
More detailed code of how I delete:
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This works fine however if I get a push notification (i.e a new message received) whilst deleting the app will crash and display an error like:
Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit/UIKit-3347.44/UITableView.m:1327
2015-07-04 19:12:48.623 myapp[319:24083] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'attempt to delete row 1 from section 0 which only contains 1 rows
before the update'
I suspect this is because my data source is changing, the size of the array that
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
references while deleting won't be consistent because it was incremented by one when the push notification triggered a refresh. Is there any way I can work around this? Am I correct that deleteRowsAtIndexPaths uses the numberOfRowsInSection method?
So, in order to solve your problem you need to ensure that your data source will not change while some table view animations are in place. I would propose to do the following.
First, create two arrays: messagesToDelete and messagesToInsert. These will hold information about which messages you want to delete/insert.
Second, add a Boolean property updatingTable to your table view data source.
Third, add the following functions:
-(void)updateTableIfPossible {
if (!updatingTable) {
updatingTable = [self updateTableViewWithNewUpdates];
}
}
-(BOOL)updateTableViewWithNewUpdates {
if ((messagesToDelete.count == 0)&&(messagesToInsert.count==0)) {
return false;
}
NSMutableArray *indexPathsForMessagesThatNeedDelete = [[NSMutableArray alloc] init];
NSMutableArray *indexPathsForMessagesThatNeedInsert = [[NSMutableArray alloc] init];
// for deletion you need to use original messages to ensure
// that you get correct index paths if there are multiple rows to delete
NSMutableArray *oldMessages = [self.messages copy];
for (id message in messagesToDelete) {
int index = (int)[self.oldMessages indexOfObject:message];
[self.messages removeObject:message];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[indexPathsForMessagesThatNeedDelete addObject:indexPath];
}
for (id message in messagesToInsert) {
[self.messages insertObject:message atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[indexPathsForMessagesThatNeedInsert addObject:indexPath];
}
[messagesToDelete removeAllObjects];
[messagesToInsert removeAllObjects];
// at this point your messages array contains
// all messages which should be displayed at CURRENT time
// now do the following
[CATransaction begin];
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexPathsForMessagesThatNeedDelete withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:indexPathsForMessagesThatNeedInsert withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
return true;
}
Lastly, you need to have the following code in all functions which want to add/delete rows.
To add message
[self.messagesToInsert addObject:message];
[self updateTableIfPossible];
To delete message
[self.messagesToDelete addObject:message];
[self updateTableIfPossible];
What this code does is ensures stability of your data source. Whenever there is a change you add the messages that need to be inserted/deleted into arrays (messagesToDelete and messagesToDelete). You then call a function updateTableIfPossible which will update the table view's data source (and will animate the change) provided that there is no current animation in progress. If there is an animation in progress it will do nothing at this stage.
However, because we have added a completion
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
at the end of the animations our data source will check if there are any new changes that need to be applied to the table view and, if so, it will update the animation.
This is a much safer way of updating your data source. Please let me know if it works for you.
Delete a Row
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
Delete a Section
Note : if your TableView have multiple section than you have to delete whole section when section contain only one row instead of deleting row
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexSet indexSetWithIndex:0];
[tableView beginUpdates];
[tableView deleteSections:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
I have a few test about your code above. And you cannot do that at the same time the least we can do is something like:
//This will wait for `deleteRowsAtIndexPaths:indexes` before the `setCompletionBlock `
//
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[tableView reloadData];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
In your code whe have:
/*
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
*/
This is not safe because if if the notification is triggered and for some reason [self.tableView reloadData]; called right before deleteRowsAtIndexPaths that will cause an crash, because the tableView is currently updating the datas then interrupt by the deleteRowsAtIndexPaths:, try this sequence to check:
/*
...
[self.tableView reloadData];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This will cause a crash...
*/
Hmm.. Back to your code, Let's have a simulation that could cause the crash.. THIS is just an assumption though so this is not 100% sure (99%) only. :)
Let assume self.messageToDelete is equal to nil;
int index = (int)[self.messages indexOfObject: nil];
// since you casted this to (int) with would be 0, so `index = 0`
[self.messages removeObject: nil];
// self.messages will remove nil object which is non existing therefore
// self.messages is not updated/altered/changed
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
// indexPath.row == 0
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
// then you attempted to delete index indexPath at row 0 in section 0
// but the datasource is not updated meaning this will crash..
//
// Same thing will happen if `self.messageToDelete` is not existing in your `self.messages `
My suggestion is check the self.messageToDelete first:
if ([self.messages containsObject:self.messageToDelete])
{
// your delete code...
}
Hope this is helpful, Cheers! ;)
'attempt to delete row 1 from section 0 which only contains 1 rows before the update'
This says your index path, during deletion, references row 1. However row 1 doesn't exist. Only row 0 exists (like the section, they are zero-based).
So how are you getting an indexPath one greater than the number of elements?
Can we see your notification handler that inserts a row? You could do some hack where if the animation is in process, you performSelector:withDelay: during the notification processing to allow time for the animation to complete.
In you code, the reason for crash is : you are updating array but not update data source of UITableView. so your dataSource also needs to reflect the changes by the time endUpdates is called.
So as per Apple Documentation.
ManageInsertDeleteRow
To animate a batch insertion, deletion, and reloading of rows and sections, call the corresponding methods within an animation block defined by successive calls to beginUpdates and endUpdates. If you don’t call the insertion, deletion, and reloading methods within this block, row and section indexes may be invalid. Calls to beginUpdates and endUpdates can be nested; all indexes are treated as if there were only the outer update block.
At the conclusion of a block—that is, after endUpdates returns—the table view queries its data source and delegate as usual for row and section data. Thus the collection objects backing the table view should be updated to reflect the new or removed rows or sections.
Example:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
Hope this help you.
I think the answer is alluded to in many of the comments here, but I'll try and tease it out a bit.
The first problem is that it doesn't look like you are bracketing your deletion method call in the beginUpdates/endUpdates methods. This is the first thing I would fix, otherwise as warned in Apple's documentation things can go wrong. (That doesn't mean they will go wrong, you're just safer doing it this way.)
Once you've done that, the important thing is that calling endUpdates checks the data source to make sure that any insertions or deletions are accounted for by calling numberOfRows. For example, if you delete a row (as you are doing), when you call endUpdates you best be sure that you've also deleted an item from the data source. It looks like you are doing that part right because you are removing an item from self.messages, which I assume is your data source.
(Incidentally you are correct that calling deleteRows... on its own without bracketing in beginUpdates/endUpdates also calls numberOfRows on the data source, which you can easily check by putting a break point in it. But again, don't do this, always use beginUpdates/endUpdates.)
Now it is possible that between calling deleteRows... and endUpdates someone else is modifying the self.messages array by adding an object to it, but I don't know if I really believe that because it would have to be extremely unfortunately timed. It's far more likely that when you receive a push message you aren't handling the insertion of the row properly, again by not using beginUpdates/endUpdates, or by doing something else wrong. It would be helpful if you could post the part of the code that handles the insertion.
If that part looks OK then it does look like you are very unfortunately making changes to the self.messages array on a different thread while the deletion code is being called. You can check this by adding a log line where your insertion code adds a message to the array:
NSLog(#"Running on %# thread", [NSThread currentThread]);
or just putting a break point in it and seeing which thread you end up on. If that is indeed the problem, you can dispatch the code that modifies the array and inserts the row onto the main thread (where it should be anyway since it's a UI operation) by doing something like below:
dispatch_async(dispatch_get_main_queue(), ^{
[self.messages addObject:newMessage];
[self.tableView beginUpdates];
[self.tableView insertRows...]
[self.tableView endUpdates];
});
That will ensure that the messages array won't get mutated by another thread while the main thread is busy deleting the row.

Assertion failure in -[UITableView _endCellAnimationsWithContext:

I'm an amateur at best, and stuck on this error! surely something simple...
- (void)addTapped:(id)sender {
TechToolboxDoc *newDoc = [[TechToolboxDoc alloc] initWithTitle:#"New Item" thumbImage:nil fullImage:nil];
[_itemArray addObject:newDoc];
//[self.tableView beginUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_itemArray.count-1 inSection:0];
NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
NSLog(#"%#",indexPath);
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
[self performSegueWithIdentifier:#"MySegue" sender:self];
//[self.tableView endUpdates];
it is breaking on the line the says
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
You need to add [UITableView beginUpdates] and [UITableView endUpdates] around:
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
From the class reference:
Note the behavior of this method when it is called in an animation
block defined by the beginUpdates and endUpdates methods. UITableView
defers any insertions of rows or sections until after it has handled
the deletions of rows or sections. This happens regardless of ordering
of the insertion and deletion method calls. This is unlike inserting
or removing an item in a mutable array, where the operation can affect
the array index used for the successive insertion or removal
operation. For more on this subject, see Batch Insertion, Deletion,
and Reloading of Rows and Sections in Table View Programming Guide for
iOS.
I think you are inserting the row in your tableView but not updating the number of rows at section that's why you are getting error in this line. So along with inserting the row You should also increase the array count or whatever you are using to show the number of rows in table view.
In my case, I was using Parse and I deleted a few users (one of which was my iPhone simulator). I got this error whenever refreshing the tableView.
I just deleted the app from the Simulator and made a new account and it works flawlessly.
#droppy's answer helped give me the lightbulb moment of what was wrong!
Hope that helps someone.

Add cells to my tableView when I make the scroll on it and finish at the bottom of it

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.

add a new item to the UITable view vs. reloadData

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).

Pattern for Updating UITableView on Sync?

I'm working on a feed reader iOS project. Feed entries are listed in a UITableView in reverse chronological order. On launch, they're loaded from a database into an array.
When the app syncs to feeds, it creates a new array for the new order of things, and then to update the table, compares the new array to the old array to determine what cells to delete, update, or insert. The way I've done it is naïve, and therefor really inefficient: Lots of calls to indexOfObject: to see if an item in one array is in the other array. Twice. once for each new entry as it's added to the new array, to see if it's in the old array, and then once for each entry in the old array, to see if it's not in the new array.
As a database professional, this design offends me.
But it must be a pretty common pattern. What would be the most appropriate, sufficiently Cocoa-ish way to go about this?
Turns out I was coming at this wrong. The solution I found was to add and remove items from the array as usual, and then to call insertRowsAtIndexPaths:withRowAnimation:, reloadRowsAtIndexPaths:withRowAnimation:, and deleteRowsAtIndexPaths:withRowAnimation: as appropriate for each row added, changed, or moved. The fault in my previous plan was the thought that I should wait until all the changes and been made, and then call each of those methods only once in a beginUpdates/endUpdates block. Turns out the block wasn't actually necessary, as the modification methods can be called outside of them.
It was much easier to call each method once for each cell inserted, updated, or deleted, than to calculate all the changes at the end and commit them at once. Was just too confusing, error-prone, and inefficient to try to do it all at once.
So the code I ended up with looks like this:
if (parsedItem.savedState == ItemModelSavedStateInserted) {
// It's a new entry. Insert it.
[items addObject:parsedItem];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:items.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
} else {
// It's an existing entry. Find it in the portal and move it, if necessary.
NSUInteger foundAt = [items
indexOfObject:parsedItem
inRange:NSMakeRange(currentItemIndex, items.count - currentItemIndex - 1)
];
if (foundAt == currentItemIndex) {
// It hasn't moved!
if (parsedItem.savedState == ItemModelSavedStateUpdated) {
// It was updated, so replace it.
[items replaceObjectAtIndex:currentItemIndex withObject:parsedItem];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:currentItemIndex inSection:0]] withRowAnimation:UITableViewRowAnimationMiddle];
}
} else {
// It has shifted position.
if (foundAt != NSNotFound) {
// It has moved.
[items removeObjectAtIndex:foundAt];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:foundAt inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
}
// Need to insert it.
[items insertObject:parsedItem atIndex:currentItemIndex];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:currentItemIndex inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
}
}
Consider using NSSets for differencing the set of current items and the set of new items, with a single NSMutableArray to hold the ordered current list. You would probably want to remove each of the expired items from the array, then insort each of the unexpired new items into the array. The items that you needed neither to remove nor to insort are the items that you may want to update.

Resources