UITableView adding rows with animation one at a time with notable delay - iphone

I am trying to add 10 rows to a tableView but instead of reloadData i m using insertRowsAtIndesPaths in a for loop, but instead of adding one row at a time, it adds all 10 at the end. Heres my code...
if([[[[[notification userInfo] objectForKey:#"cityevent"]objectForKey:#"events"]objectForKey:#"event"] isKindOfClass:[NSArray class]])
{
[self.featuredEventTableView beginUpdates];
for(NSDictionary *tempDict in [[[[notification userInfo] objectForKey:#"cityevent"]objectForKey:#"events"]objectForKey:#"event"])
{
ESEvent *event=[[ESEvent alloc]init];
event.name=[tempDict objectForKey:#"name"];
if([[tempDict objectForKey:#"image"]isKindOfClass:[NSNull class]] || [tempDict objectForKey:#"image"] ==nil)
event.image=[UIImage imageNamed:#"category_default.png"];
else
{
event.image=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[tempDict objectForKey:#"image"]]]];
}
[events addObject:event];
NSIndexPath *ip=[NSIndexPath indexPathForRow:([events count]-1) inSection:0];
NSLog(#"row: %i section: %i",ip.row,ip.section);
[self.featuredEventTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:ip] withRowAnimation:UITableViewRowAnimationRight];
[self.featuredEventTableView endUpdates];
}
}
Any suggestions?

Move your start updates call to just before the insertRowsAtIndexPaths: call. See if that helps. I'm surprised this works at all because you call end update multiple times but start only once.
Actually this will probably still be so fast that you don't even notice. Look into NSTimer or GCD's dispatch_after to stagger the updates.

The for loop doesn't matter in this case - the OS accumulates all animation actions and commits them together. To do this right, you'll have to use animation stop selector and call an external method to add the next cell until all cells have been added.

why you are not using reloadData ... I guess this will help you in what you want to display on your UI. If you use for loop and add one data into your actual array and at end if you call reload, it will give you that effect.
If you want to continue with your existing code than try adding [NSThread sleepForTimeInterval:0.5]; after i guess [self.featuredEventTableView endUpdates];. I m not on my mac so can't test it. This will pause the current thread for some time, so it will give that animation effect.
EDIT Try code below
if([[[[[notification userInfo] objectForKey:#"cityevent"]objectForKey:#"events"]objectForKey:#"event"] isKindOfClass:[NSArray class]])
{
for(NSDictionary *tempDict in [[[[notification userInfo] objectForKey:#"cityevent"]objectForKey:#"events"]objectForKey:#"event"])
{
ESEvent *event=[[ESEvent alloc]init];
event.name=[tempDict objectForKey:#"name"];
if([[tempDict objectForKey:#"image"]isKindOfClass:[NSNull class]] || [tempDict objectForKey:#"image"] ==nil)
event.image=[UIImage imageNamed:#"category_default.png"];
else
{
event.image=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[tempDict objectForKey:#"image"]]]];
}
[events addObject:event];
NSIndexPath *ip=[NSIndexPath indexPathForRow:([events count]-1) inSection:0];
NSLog(#"row: %i section: %i",ip.row,ip.section);
// If this don't work then add thread sleep code at here.
[self.featuredEventTableView beginUpdates];
[self.featuredEventTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:ip] withRowAnimation:UITableViewRowAnimationRight];
[self.featuredEventTableView endUpdates];
}
}

None of the answers worked. I used NSTimer as follows...
tableTimer=[NSTimer scheduledTimerWithTimeInterval:0.4f target:self selector:#selector(performTableUpdates:) userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:i],#"count", nil] repeats:YES];
and
-(void)performTableUpdates:(NSTimer*)timer
{
NSLog(#"%i",i);
NSIndexPath *ip=[NSIndexPath indexPathForRow:i inSection:0];
[newArray addObject:[events objectAtIndex:i]];
[self.featuredEventTableView beginUpdates];
[self.featuredEventTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:ip] withRowAnimation:UITableViewRowAnimationLeft];
[self.featuredEventTableView endUpdates];
if(i<[events count])
{
if([events count]-i==1)
{
[tableTimer invalidate];
i++;
}
else
i++;
}
}

Related

App crashes when refreshing table (Probably the call endUpdates on tableView causes the crash)

Came across this post but did not fix my problem.
iPhone app crashing on [self.tableView endUpdates]
Having a UITableView which loads articles from a newspaper website. First load works
as expected but when I use an UIRefreshControl to fetch the articles again my app crashes
when (animating) inserting rows.
Error:
Code:
- (void)insertRowsWithAnimation
{
NSMutableArray *indexPaths = [NSMutableArray array];
NSInteger i = self.latestArticlesArray.count - self.latestArticlesArray.count;
for (NSDictionary *dict in self.latestArticlesArray) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
i++;
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationMiddle];
[self.tableView endUpdates];
}
- (void)fetchEntries
{
UIView *currentTitleView = [[self navigationItem] titleView];
UIActivityIndicatorView *aiView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[[self navigationItem] setTitleView:aiView];
[aiView startAnimating];
void (^completionBlock) (NSArray *array, NSError *err) = ^(NSArray *array, NSError *err) {
if (!err) {
[[self navigationItem] setTitleView:currentTitleView];
self.latestArticlesArray = [NSArray array];
self.latestArticlesArray = array;
[self insertRowsWithAnimation];
}
};
[[Store sharedStore] fetchArticlesWithCompletion:completionBlock];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.latestArticlesArray.count;
}
If you want to see more methods please let me know. Hope you can help. From what I've learned so far I think the number of posts have changed therefore the table expects another amount of articles to show?
The line
NSInteger i = self.latestArticlesArray.count - self.latestArticlesArray.count;
sets i to zero, therefore insertRowsAtIndexPaths is called with an empty array.
I assume that your intention was to call insertRowsAtIndexPaths with the row numbers of the newly added rows, but then you have to remember the old data before replacing the array in
self.latestArticlesArray = array;
But note that since you replace the entire array, you can as well call
[self.tableView reloadData];
instead of beginUpdates/insertRowsAtIndexPaths/endUpdates.
Update: My first analysis is wrong (and wattson12's is correct). As you said in the comments, you need just a simple animation that removes all previous rows and inserts the new rows after a fetch. This can be done like this:
- (void)replaceArticlesWithAnimation:(NSArray *)articles
{
NSUInteger oldCount = [self.latestArticlesArray count];
self.latestArticlesArray = articles;
NSUInteger newCount = [self.latestArticlesArray count];
[self.tableView beginUpdates];
for (NSUInteger i = 0; i < oldCount; i++) {
[self.tableView deleteRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
}
for (NSUInteger i = 0; i < newCount; i++) {
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
}
[self.tableView endUpdates];
}
and in fetchEntries you call
if (!err) {
[[self navigationItem] setTitleView:currentTitleView];
[self replaceArticlesWithAnimation:array];
}
The reason it loads the first time is because you are inserting a number of rows each time, so on the first run the number of rows changes from 0 to the count in array (in the completion block), and you call insertRows with a number of index paths equal to the count of the array.
The 2nd time you call it you are inserting new rows, but you are not updating the count to reflect the new sum. You should be adding your existing array to the one returned in the completion block, and returning the count of that combined array in numberOfRowsInSection
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
});

NSInvalidArgumentException when deleting cell using a different class

Update for code: Following on Matthew's answer I tried correcting my code to be more correct. Now the code does delete the cell but also crashes and gives an error:
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[0]'
The code below is from an action called checkboxTapped which is in my CustomCell code. Once action is fired it gives the error. I figured out that my indexPath is equal to NULL, and thats most likely the issue. But I don't know how to fix it.
[self.textLabel setTextColor:[UIColor grayColor]];
[self.detailTextLabel setTextColor:[UIColor grayColor]];
parent = [[ViewController alloc] init];
db = [[DataObject alloc] init];
NSIndexPath *indexPath = [[parent tableView] indexPathForSelectedRow];
[[parent array] removeObjectAtIndex:[indexPath row]];
[db deleteTaskAtIndex:[indexPath row]];
[[parent tableView] deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[db release];
[parent release];
Old: I looked through my code and I printed my array I was using and it appears to be fine, yet this error still persists.
* Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM removeObjectAtIndex:]: index 1 beyond bounds [0 .. 0]'
My guess was that it had something to do with my indexPath but it doesn't make much different how much I change it.
-(void)checkboxTapped:(id)sender
{
[sender setSelected:YES];
[self.textLabel setTextColor:[UIColor grayColor]];
[self.detailTextLabel setTextColor:[UIColor grayColor]];
parent = [[ViewController alloc] init];
UITableView *tableView = parent.tableView;
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:parent.array];
[parent release];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[array count] inSection:1];
[array removeObjectAtIndex:[indexPath row]];
[db deleteTaskAtIndex:[indexPath row]];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[array release];
[tableView endUpdates];
[tableView reloadData];
}
In your code [indexPath row] is going to return the value of [array count]. That's unlikely to be what you want. If your array has zero objects in it, you are going to attempt to remove the object at index 0. But there will be no objects and you'll get an error. If your array has 1 object in it, you're going to attempt to remove the object at index 1. Again, that will fail, because there is no object at index 1, just one object at index 0.
If you want to remove the last object in an array you need to use an index that is count-1. You may also need to check to see if the array is empty, if that case can occur.
Updated in response to follow up in comment
You don't want to do anything indexPathWithIndex. As a first step, try modifying your code along the following lines:
-(void)checkboxTapped:(id)sender
{
[sender setSelected:YES];
[self.textLabel setTextColor:[UIColor grayColor]];
[self.detailTextLabel setTextColor:[UIColor grayColor]];
parent = [[ViewController alloc] init]; // looks very odd - is an instance of this viewController active when the checkBox is tapped? If so, you don't want to create a new one, you want to access the existing one
UITableView *tableView = parent.tableView;
[parent release]; // this looks very dicey - when you release the parent, won't it release the tableView too?!
int lastRow = [array count] - 1;
if (lastRow == 0)
{
return; // bail if there are no rows in the table
}
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:parent.array];
[array removeObjectAtIndex: lastRow]; // not clear this will do anything as the reference to array is discarded later
[db deleteTaskAtIndex: lastRow];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow: lastRow inSection:1];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[array release];
// [tableView endUpdates]; // there's no matching beginUpdates and you're only do one change operation anyway - leave this out
// [tableView reloadData]; // if you leave this line in, you won't see the delete animation - if you just want to delete one row, you wouldn't normally use reloadData, at least not if you want the animation
}
All this said, it looks as if there are other things going on here.
What's happening with array? You create this, remove an item from it and the discard the pointer to it. Is that what you really want to do. A more common pattern would be to get the pointer to the array from the other object and remove the item at the end of it here.
It's not clear from your code how you are updating the table's data source. When using deleteRowsAtIndexPaths:withRownAnimation you need to make sure the table's data source will return one row less than it did last time it was asked with tableView:numberOfRowsInSection:. From your code it's not clear how the tableView dataSource is going to know there's one less item, unless, perhaps, it's looking at whatever it is that db is pointing to in order to find this out.
More fundamentally, with a typical design pattern the tableView is going to be released when you release the parent view, so whatever it points to after `[parent release]' is going to do something undefined and is likely to crash at least some of the time.

Deleting rows in uitableview

I am Having an application where, if the user enters data the rows will be updated with that data
Can i use One Single Button say 'delete' which when clicked will delete all the rows in the tableview at once.?
Yes you can do that. First remove all data from your data source, then reload your table. For ex. -
[yourArrayDataSource removeAllObjects];
[yourTable reloadData];
To animate the deletion of rows - do this in an IBAction method & link it to your UIButton. As soon as you press the button you will have a smooth awesome animation making all your rows fade out.
-(IBAction)deleteRows
{
[yourTable beginUpdates];
for(int i=0; i<[yourArrayDataSource count]; i++)
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.searchResTable deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
[yourTable endUpdates];
}
There are various animations that you can use here-
UITableViewRowAnimationBottom
UITableViewRowAnimationFade
UITableViewRowAnimationMiddle
UITableViewRowAnimationNone
UITableViewRowAnimationRight
UITableViewRowAnimationTop
make a button and in the button action method
-(IBAction)deleteRows
{
[array removeAllObjects];
[tableview reloadData];
}
Srikar's answer put me on the right track, but creates a lot of extra single item arrays, and calls deleteRowsAtIndexPaths far more than is needed.
-(void)clearTable
{
NSMutableArray *indexPaths = [NSMutableArray array];
for(int i=0; i<[self.myArray count]; i++)
{
NSIndexPath *anIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[indexPaths addObject:anIndexPath];
}
[self.myTableView beginUpdates];
[self.myTableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
self.myArray = [NSArray array];
[self.myTableView endUpdates];
}

RowAnimation looks weird with different height cells

I have a problem with animating deletes and inserts on a UITableView. The Fact ist, that I have different cell heights, some 44.0 some 135.0 now. I have uitableviewstylegrouped with different sections. The first row of each sections is a grouping row. On click I remove all rows of this section except the grouping row. But that animation looks weird, when animating (UITableViewRowAnimationTop) the 135px height cell. I tried to set self.tableview.cellheight to 135 and commented out the tableView:cellHeightForIndexPath-Method. And the Animation works fine. Or I set every cellheight to 135 in tableView:cellHeightForIndexPath-Method.
It looks like the animation process checks the height of the first row in the sections an takes that height of the cell for all following cells to animate.
Somebody has an idea?
- (void) showhideGroup:(int)group
{
NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
MCGroup *handleGroup = [groups objectAtIndex:group];
NSMutableArray *groupRows = [visibleGroupData objectForKey:handleGroup.title];
[self.tableView beginUpdates];
if (!handleGroup.hidden) {
int row = 0;
for(MobileFieldMapping *field in groupRows)
{
if (![field.datatype matchesInsensitive:GROUPING_CELL])
{
NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:group];
[indexPaths addObject:path];
}
row++;
}
row = 0;
for(NSIndexPath *index in indexPaths)
{
[groupRows removeObjectAtIndex:index.row-row];
row++;
}
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop];
handleGroup.hidden=YES;
}
else
{
NSMutableArray *allGroupRows = [groupData objectForKey:handleGroup.title];
int row = 0;
for (MobileFieldMapping *field in allGroupRows)
{
if (![groupRows containsObject:field])
{
NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:group];
[indexPaths addObject:path];
[groupRows insertObject:field atIndex:row];
}
row++;
}
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop];
handleGroup.hidden=NO;
}
[self.tableView endUpdates];
[indexPaths release];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
if ([cell isKindOfClass:[FormCell class]])
{
return [(FormCell*)cell height];
}
return 44.0;
}
I had the same problem.
My workaround is to use fade section animation:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
Instead of one for elements:
[tableView deleteRowsAtIndexPaths:tmpArray withRowAnimation:UITableViewRowAnimationTop];
This looks much smoother.
An old one that is still unanswered. I assume you figured it out a long time ago, but here is my first thought. (I recall trying to do something like this myself.)
When the UITableView calls heightForRowAtIndexPath:, it will be because the table is trying to prepare the cell, so you can't use the cell to return the height. This may cause an infinite regression in calls or it may detect this and just give up or throw an exception and leave you holding the bag.
You must calculate the cell height without using the cell itself.
As for the cell height assumption, try calling insert/delete for individual cells rather than a single call to do it all at once. The UITableView will still batch them up for the animation after you call endUpdates.

UISegmentedControl Changed Event misfires when adding a method inside

Here is my code for the UIControlEventValueChanged event for a UISegmentedControl:
- (void)segmentAction:(id)sender{
if([sender selectedSegmentIndex] == 0){
pendingIsShowing = YES;
[freeCutsTable reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
}else if([sender selectedSegmentIndex] == 1){
pendingIsShowing = NO;
[self showAvailableCuts:sender];
[freeCutsTable reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
}
}
My problem is when the value is changed, the [self showAvailableCuts:sender] is called, but the segment control no longer changes its index. If I comment out the method call, it works fine...What I tried to do was pass in the segmented control into [self showAvailableCuts:sender] and change it that way, but to no avail...
- (void)showAvailableCuts:(id)sender{
if(!pendingIsShowing){
NSString *path =#"https://WebserviceURL";
NSString* escapedUrl = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
//[sender setSelectedIndex:1];
NSLog(#"%#", escapedUrl);
[self parseXMLFileAtURL:escapedUrl];
}
}
I'm not sure why this is occurring...
Without looking at the code for [self showAvailableCuts:sender]; all I can think of is - why do you need to pass the 'sender' object itself? Just modify the showAvailableCuts method so that you need to pass only the required value(s) like [sender titleForSegmentAtIndex:] , [sender selectedSegmentIndex] etc.
ok, firstly the setter should be [sender setSelectedSegmentIndex:index] not what you have commented out. I have myself successfully used this -
sender.selectedSegmentIndex = index; //in your case index is 1