Smooth animation of UITableView when removing section - ios

tl;dr
When deleting a section in a UITableView while the scroll offset is somewhere in the middle of the table the flow of animation goes like this -
The contentOffset is set to (0,0) immediately (no animation, just pops up)
The section fades away nicely
I'd like to make this animation flow a bit better - fade away the section and only afterwards (or simultaneously in a smooth way) scroll the "dead zone" of the table back up.
A bit more explaining
I'm using NSFetchedResultsController as a data source for UITableView to display rows and update the table when changes occur in the NSManagedObjectContext - like this (I removed unrelated code) -
- (void)controller: (NSFetchedResultsController *)controller
didChangeObject: (id)anObject
atIndexPath: (NSIndexPath *)indexPath
forChangeType: (NSFetchedResultsChangeType)type
newIndexPath: (NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableController.tableView;
switch(type) {
....
NSFetchedResultsChangeDelete:[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
...
}
}
I got all the boilerplate of controllerWillChangeContent and controllerDidChangeContent, the result of this code is that if all the rows in a specific section are removed - the section is also removed.
The problem (as I specified in the tl;dr section) is that the animation doesn't work as expected -
If the section removal happen while scrolled half-way into the removed section, the scroll content changed immediately and the section fades away, which looks pretty broken.
Anyone ever stumbled on a situation like this? I'm sure I can narrow it down to a generic problem without using NSFetchedResultsController, that's jus the code I'm currently using.
I'd gladly add more information if needed.
Thanks!
Update 1
So after a bit playing with the contentOffset manually I can get something partly working when doing this flow -
When the NSFetchedResultsController calls controllerWillChangeContent I save the UITableView contentOffset (before beginUpdates)
When the controllerDidChangeContent is called and right after I call endUpdates I save the contentOffset of the table (this is the offset that wasn't animated)
I scroll back to the original contentOffset I saved in part 1 and use [tableView setContentOffset:offsetAfterEndUpdates animated:YES] to scroll to the new offset
This cannot be the best solution / what Apple ment.

A UITableViewDelegate is also conform to UIScrollViewDelegate protocol so you could delay that section removal action to
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
// try to perform your action here,
// for instance you could re-attach your NSFetchedResultController delegate here
}

One approach that works (at least in my experience) is to a) delete the section and b) don't delete the individual rows.
For reference, take a look at the TLIndexPathUpdates initializer in TLIndexPathTools. It calculates batch updates and I think it works well for the scenario you've described.

Okay!
So after intensive digging I found that the problem lies in my code (like I was commented right after posting). Was pretty hard to find but somewhere between the beginUpdates and endUpdates I change the UITableView footer view, that messes with the contentOffset animation.
So that's it, my fault, not Apple.
Thanks for the help Jonathan Cichon!

Related

Refreshing the view of UITableView

I understand the concepts of cell re-usability for Xcode 5.0 table views. However, I have one very weird observation which I don't understand and wish anyone of you could enlighten me here. Thanks.
I have implemented a table view with a search bar utility (just on top of the table view). Under each custom cell (prototype cell), whenever a user clicks on it, it will be marked with a checkmark (UITableViewCellAccessoryCheckmark). The number of cells are more than 10.
Observation:
- Without using any search, marking and unmarking a cell is working as intended. Cells are updated instantly along with their checkmarks.
- When doing a search, from the results given, marking and unmarking a cell is also working as intended.
[Problem] Here comes the weird issue: when cancelling a search, an already marked cell (marked during search) does not refresh itself in the tableview unless scrolling up or down is performed!
And hence, I wrote [tableview reload] at the end of tableview:didSelectRowAtIndexPath: method. Obviously, it doesn't refresh the tableview for me. Without further changing any other code, merely modifying [tableview reload] to [self.tableview reload] under the same method works!
Why is the only addition of "self." able to make the table cells refreshed instantly? I have always thought the first argument, tableView, from the method (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath is as equal to self.tableview. Obviously, my interpretation in this case is wrong.
Thank you. I'm sorry for my lengthy post.
My guess is that this UISearchBar comes from a UISearchDisplayController. Is that correct?
If true, that is a common misconception, but an easy one to understand.
When filtering your UITableView entries and showing results, UISearchDisplayController actually overlays the view with its own tableView, UISearchResultsTableView.
Thus, this overlaid tableView also gets to call data source and delegate methods on your implementation, and this is when the tableView argument from tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath stops being equivalent to self.tableView.
This means that calling [tableView reloadData] during filtering actually asks UISearchResultsTableView to reload its contents, not self.tableView, a property of your viewController.

Implementing estimatedHeightForRowAtIndexPath: causes the tableView to scroll down while reloading

I've implemented 'infinite scrolling' on one of my projects and I was playing around with the new estimatedHeightForRowAtIndexPath: delegate method. Once I implemented that delegate method, my tableView jumps (a.k.a scroll to the bottom) whenever i call reloadData (which happens when i add a new set of rows.)
Without that method, my tableView stays in place and it adds the additional rows to the bottom of the tableView without any scrolling.
I'm calling the [tableView reloadData] and not the other methods (insertRowsAtIndexPaths:). I don't call the beginUpdates or endUpdates since I'm reloading the whole table.
Has anyone experienced this ? I
So here's what i did to reduce the jumping while reloading the tableView.
In the estimatedHeightForRowAtIndexPath: I started returning a better estimated height. The more accurate the height is, lesser the jumping while adding new rows to the bottom.
I also started caching the calculated height of the cell from heightForRowAtIndexPath: in a dictionary and return the cached value the next time it calls the estimatedHeightForRowAtIndexPath:.
Hope this helps anyone who encounters this issue.
I filed a BugReport quite a while ago (16472265) and today got the response that it's fixed in iOS8. Tried it out and it works now as expected. No more jumping of the tableview! Wohoooo
Check your condition in this scrollView Delagete method,
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

UITableView insertRows without locking main thread

I'm trying to implement infinite scroll like the Facebook app has.
It appears that Facebook is somehow inserting rows or reloading the table view without stopping the current scroll.
Does anyone know how they're achieving this?
When I call insert rows or reload data during a scroll the scroll is stopped dead. Any help would be appreciated.
Not much code to provide just typical UITableView functions.
[self.tableView insertRowsAtIndexPaths:self.paths withRowAnimation:UITableViewRowAnimationNone];
or
[self.tableView reloadData];
self.paths being the paths recently added via the next page request.
I've also added the following method
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;
Which is working correctly now with reloadData being called. Unfortunately even while this speeds up the addition of cells it still locks the current scroll.
Okay so, as far as the stopping of the currently active scroll is concerned the problem came down to a single line of code. And the solution was just as small.
My TableView has a refresh control on it. In the method in which I receive the next set of data for the TableView I would call the following.
[refreshControl endRefreshing];
My assumption was if it wasn't refreshing that endRefreshing would simply do nothing.
Well... Apparently that's not the case. When you call endRefreshing on a refresh control that isn't refreshing it'll stop the tableview scroll. My fix is as follows.
if(refreshControl.isRefreshing){
[refreshControl endRefreshing];
}
Now the scroll continues after the next load albeit with some delay. That delay of course is easier to debug.
That's just the about of overhead involved in calculating the height of the cells being added which is called on all cells when reload is called unless you have
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;
So now that I have everything sorted out it's time to create an efficient way to calculate a reasonably close estimate height.

UITableView stop updating UI without crashing after adding data

I have a problem that is driving me crazy.
I have an UITableView that is always in editing mode (it has to be).
The user can add new rows to it.
The navigationItem.leftbarButton of the tableViewController pushes a new controller just to do it, let's call it "newRowsVC".
Before the push the tableViewController set itself as the delegate of the newRowsVC, the protocol has only a method:
-(void) aNewRowHasBeenCreated
{
[self.tableView reloadData];
}
I start adding rows and everything works fine, each now row is immediately displayed in the tableViewController until the last new row will force the tableView to scroll because there won't be anymore screen real estate for it. Here, I have no idea why, the tableView, only it, is as frozen, with no scrollbar and doesn't respond to input. The app continues to run without a crash and I can even dismiss the tableViewController by tapping the navigationItem.rightbarButtonItem.
I can keep creating new rows, they are added to the array, the number of row in the tableView data source is computed correctly. But the table is like dead.
If I dismiss the tableViewController and then I come back to it, I see that all the rows previously created, also the ones not shown as soon as they were created are there!
I really do not have idea of how I can fix this.
The first thing I tried was to force the scroll after the reload of the table but it didn't fix it.
-(void) aNewRowHasBeenCreated
{
[self.tableView reloadData];
[self.tableView setScrollEnabled:YES];
}
I also tried forcing the tableView to scroll to the last row but it didn't fix it.
-(void) aNewRowHasBeenCreated
{
[self.tableView reloadData];
[self.tableView setScrollEnabled: YES];
[self.tableView scrollToRowAtIndexPosition: [self.tableView indexPathsForVisibleRows]last object] atScrollPosition: UITableViewScrollPositionBottom animated:YES];
}
I check the number of rows each time the table is reloaded. No error, the number is perfect and the new rows data are correctly in the array, also the data for the cells not shown.
I thought it could be because the tableView is always in editing mode so I tried setting it to NO but nothing changes, id din't fix the problem.
Two notes:
1)the tableView has to be the delegate of each one of it's custom cells. They have an UITextField and an UIStepper, handled by the tableViewController. I already tried to not set the tableViewController as the delegate of its custom cells but nothing changes so the problem is not this.
2) self.tableView.bounces = NO but this has nothing to do with the scrolling issue, the scroll is enabled.
Update: After more tests I found that if I remove the dequeue of the reusable custom cell everything works fine, so the problem should be about the reuse.
static NSString *CellIdentifier=#"MyCell"
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier];
Nicola
After hours I got it. As pointed out by Amit the problem was hidden in cellForRowAtIndexPath... It was hard to catch because it didn't happened all the times and the first times I enabled/disabled the cell reusing everything seemed the same so I did not link the problem to it. I got back on it after I tried about all the other options I had been able to think about.
The problem was in the reuse of the cells and the fact that the custom cell has the tableView as its delegate to handle its textView and the stepper without exposing them.
I got rid of all the delagion stuff and exposed the textView and the stepper as public properties of the view. By doing this I was able to set the tableViewController directly to be the delegate of the cell.textView and to add directly a target/action for the stepper.
Everything works flawslessly now.
Thanks to everyone who tried to help me!
Nicola

Middle animation in grouped table view looks horrible

I've found similar question, but there is no answer (sorry, answer just doesn't work).
So I have grouped table and I want to animate content update instead of doing [tableView reloadData].
I do that by using this piece of code:
// Data source already updated here, but reloadData wasn't called
[self.tableView beginUpdates];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationMiddle];
[self.tableView endUpdates];
I uploaded 2 examples of the animation:
Plain: http://cl.ly/3u1M3l1w3V3J (slow motion)
Grouped: http://cl.ly/1O3Z2M280n0z (slow motion)
As you can see difference is huge.
I don't change my code at all, just change tableView style in the storyboard.
Does it men that there is no other way then subclassing UITableView and UITableViewCell and implement my very own animation using CoreAnimation?
Implementing your own animation with CoreAnimation shouldn't be necessary when it comes to animating the rows of the table.
UITableView supports much more advanced animations than simply reloading a section and I suggest that you take a look at them.
Since you are shuffling the rows in your videos you should take a look at moveRowAtIndexPath:toIndexPath: (on UITableView). You put the calls to it within beginUpdates and endUpdates.
By knowing the order before and after the re-shuffle you can move all the rows into their new places and have them slide into their correct place.
It will take some thinking to figure out where each row should go but it will be much easier than rolling your completely custom solution.

Resources