UITableView scroll to bottom to see insertion animation - iphone

I have a UITableView that I am adding a row to with an animation (using insertRowsAtIndexPaths:withRowAnimation:). This is all good as long as the table is not longer than the screen.
If it is bigger than the screen then I am trying to scroll to the bottom, but it is not quite working how I want. If I scroll to the new row after it is added I miss the animation. If I try to scroll to that indexPath before it is added it throws an exception (about it not being a valid indexPath)
Is there a solution to this other than adding a blank row?

I had this same issue. Here was my solution.
First -
Update your data source
Second -
NSIndexPath *path = [NSIndexPath indexPathForRow:([arrayWhichFeeds count] - 1 inSection:0]];
NSArray *paths = [NSArray arrayWithObject:path];
[myTableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationTop];
[myTableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionBottom animated:YES];
*Please note that this IS NOT wrapped in the [tableView beginUpdades] and [tableView endUpdates] code. If you do it will not work as desired.
Give it a try, it should animate in the new rows from the bottom while scrolling to them.

Yes there is a solution without adding a blank row.
Note: In the code, I consider that there is only 1 section but many rows. You can modify the code to manage multiple sections as well.
- (void)theMethodInWhichYouInsertARowInTheTableView
{
//Add the object in the array which feeds the tableview
NSString *newStringObject = #"New Object";
[arrayWhichFeeds addObject:newStringObject];
[myTableView beginUpdates];
NSArray *paths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:([arrayWhichFeeds count] - 1) inSection:0]];
[myTableView insertRowsAtIndexPaths:paths withRowAnimation:NO];
[myTableView endUpdates];
[myTableView reloadData];
[myTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([arrayWhichFeeds count] - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

Other techniques, including those mentioned under this question did not work for me. This did however:
Add the new item to the internal dataSource collection.
Set a flag in the item that indicates it is "new". Force the display of the cell to show nothing when this flag is set.
Immediately call tableView:reloadData:
Now the new item is in this table but will visually appear empty (due to the flag).
Check to see if this new item is visible using tableView.indexPathsForVisibleRows.
If the item was is on screen immediately flip that "new" flag on the dataSource item to NO and call tableView:reloadRowsAtIndexPaths with an animation set. Now it will appear as if this item was just added. (you're done in this case)
If it wasn't on screen scroll it into view with tableView:scrollToRowAtIndexPath: but don't immediately call reloadRowsAtIndexPath...
Handle the message (void)scrollViewDidEndScrollingAnimation: and do that same reloadRowsAtIndexPath from step 6. I suspect this method is called anytime scrolling happens, so you'll have to detect when it's called from step 7 and when it's called because the user is scrolling around.
I worked this technique out (and wrote this answer) when I 1st started iOS development, but this did work out long term.

Remeber to do this in the main thread otherwise it won't scroll to the required position.

Update data source
Insert row at bottom
Scroll to bottom
Example:
YOUR_ARRAY.append("new string")
tableView.insertRows(at: [IndexPath(row: YOUR_ARRAY.count-1, section: 0)], with: .automatic)
tableView.scrollToRow(at: IndexPath(row: YOUR_ARRAY.count-1, section: 0), at: UITableViewScrollPosition.bottom, animated: true)

More generally, to scroll to the bottom:
NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:([self.table numberOfRowsInSection:([self.table numberOfSections] - 1)] - 1) inSection:([self.table numberOfSections] - 1)];
[self.table scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];

This works for me. Instead of inserting a row I just highlight it (by fading it in out.) The principle is the same, though.
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
UIView.animateWithDuration(0.5, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: false)
}, completion: {
(finished: Bool) -> Void in
// Once the scrolling is done fade the text out and back in.
UIView.animateWithDuration(5, delay: 1, options: .Repeat | .Autoreverse, animations: {
self.tableView.reloadRowsAtIndexPaths([definiteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}, completion: nil)
})
}

Related

How to setContentOffset: after insertRowAtIndexPath:

I developing an app for iOS 5.
How I can set the contentOffset after a row is inserted (insertRowAtIndexPath:) in a table view? insertRowAtIndexPath: sets the offset to 0. I have tried to set the offset in willDisplayRow: delegate method but without result.
If the issue that you you want to change the offset after the row insertion animation has completed, then call a method to set the offset after delay. This will let the current run loop finish, allowing for the row insertion animation to complete. You may want to disable interaction with the table view during the delay.
You could try adding something like the following to your tableViewController.
- (void) adjustsContentOffset: (NSIndexPath*) indexPath;
{
CGFloat offsetY = 0.0; // insert some calculation deriving your value from indexPath
[[self tableView] setContentOffset: CGPointMake(0.0, offsetY) animated: YES];
}
- (void) insertRow: (NSIndexPath*) indexPath;
{
[[self tableView] insertRowsAtIndexPaths: [NSArray arrayWithObject: indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[self performSelector: #selector(adjustsContentOffset:) withObject: indexPath afterDelay: 0.3];
}
Thanks Matthew again for your really helpful answer but finally I found another solution that i want to share with community:
Just add a empty cell at the end of a table with height that equals your desired offset

UITableView insert rows without scrolling

I have a list of data that I'm pulling from a web service. I refresh the data and I want to insert the data in the table view above the current data, but I want to keep my current scroll position in the tableview.
Right now I accomplish this by inserting a section above my current section, but it actually inserts, scrolls up, and then I have to manually scroll down. I tried disabling scrolling on the table before this, but that didn't work either.
This looks choppy and seems hacky. What is a better way to do this?
[tableView beginUpdates];
[tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
NSUInteger iContentOffset = 200; //height of inserted rows
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
If I understand your mission correctly,
I did it in this way:
if(self.tableView.contentOffset.y > ONE_OR_X_ROWS_HEIGHT_YOUDECIDE
{
self.delayOffset = self.tableView.contentOffset;
self.delayOffset = CGPointMake(self.delayOffset.x, self.delayOffset.y+ insertedRowCount * ONE_ROW_HEIGHT);
[self.tableView reloadData];
[self.tableView setContentOffset:self.delayOffset animated:NO];
}else
{
[self.tableView insertRowsAtIndexPath:indexPathArray WithRowAnimation:UITableViewRowAnimationTop];
}
With this code, If user is in the middle of the table and not the top, the uitableview will reload the new rows without animation and no scrolling.
If user is on the top of the table, he will see row insert animation.
Just pay attention in the code, I'm assuming the row's height are equal, if not , just calculate the height of all the new rows you are going to insert.
Hope that helps.
The best way I found to get my desired behavior is to not animate the insertion at all. The animations were causing the choppyness.
Instead I am calling:
[tableView reloadData];
// set the content offset to the height of inserted rows
// (2 rows * 44 points = 88 in this example)
[tableView setContentOffset:CGPointMake(0, 88)];
This makes the reload appear at the same time as the content offset change.
For further spectators looking for a Swift 3+ solution:
You need to save the current offset of the UITableView, then reload and then set the offset back on the UITableView.
func reloadTableView(_ tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
Called by: reloadTableView(self.tableView)
Just call setContentOffset before endUpdates, that works for me.
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
[tableView endUpdates];
I am using self sizing cells and the estimated row height was pretty useless because the cells can vary significantly in size. So calculating the contentOffset wasn't working for me.
The solution that I ended up with was quite simple and works perfectly.
So first up I should mention that I have some helper methods that allow me to get the data element for an index path, and the opposite - the index path for a data element.
-(void) loadMoreElements:(UIRefreshControl *) refreshControl {
NSIndexPath *topIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]
id topElement = [myModel elementAtIndexPath:topIndexPath];
// Somewhere here you'll need to tell your model to get more data
[self.tableView reloadData];
NSIndexPath *indexPath = [myModel indexPathForElement:topElement];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[refreshControl endRefreshing];
}
None of the answers here really worked for me, so I came up with my solution. The idea is that when you pull down to refresh a table view (or load it asynchronously with new data) the new cells should silently come and sit on top of the tableview without disturbing the user's current offset. So here goes a solution that works (pretty much, with a caveat)
var indexpathsToReload: [IndexPath] = [] //this should contain the new indexPaths
var height: CGFloat = 0.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
UIView.performWithoutAnimation {
self.tableview.reloadSections(NSIndexSet(index: 0) as IndexSet, with: .none)
self.tableview.layoutIfNeeded()
indexpathsToReload.forEach({ (idx) in
height += self.feed.rectForRow(at: idx).height
})
let afterContentOffset = self.tableview.contentOffset
let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + height)
self.tableview.setContentOffset(newContentOffset, animated: false)
}
}
CAVEAT (WARNING)
This technique will not work if your tableview is not "full" i.e. it only has a couple of cells in it. In that case you would need to also increase the contentSize of the tableview along with the contentOffset. I will update this answer once I figure that one out.
EXPLANATION:
Basically, we need to set the contentOffset of the tableView to a position where it was before the reload. To do this we simply calculate the total height of all the new cells that were added using a pre populated indexPath array (can be prepared when you obtain the new data and add them to the datasource), these are the indexPaths for the new cells. We then use the total height of all these new cell using rectForRow(at: indexPath), and set that as the y position of the contentOffset of the tableView after the reload. The DispatchQueue.main.asyncAfter is not necessary but I put it there because I just need to give the tableview some time to bounce back to it's original position since I am doing it on a "pull to refresh" way. Also note that in my case the afterContentOffset.y value is always 0 so I could have hard coded 0 there instead.

How to get a UITableview to go to the top of page on reload?

I am trying to get a UITableview to go to the top of the page when I reload the table data when I call the following from
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
// A bunch of code...
[TableView reloadData];
}
Along those same lines, I would also like to be able to go to a specific section when I reload the table data.
I tried placing the following, which seems applicable only to the row, before and after the reload, but nothing happens:
[TableView scrollToRowAtIndexPath:0 atScrollPosition:UITableViewScrollPositionTop animated:YES];
Here's another way you could do it:
Objective-C
[tableView setContentOffset:CGPointZero animated:YES];
Swift 3 and higher
tableView.setContentOffset(.zero, animated: true)
Probably the easiest and most straight forward way to do it.
After reading all of the answers here and elsewhere, I finally found a solution that works reliably and does not show the scrolling after the data is loaded. My answer is based on this post and my observation that you need a negative offset if you are using content insets:
func reloadAndScrollToTop() {
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.contentOffset = CGPoint(x: 0, y: -tableView.contentInset.top)
}
To scroll to a specific section you need to specify the index path to it:
NSIndexPath *topPath = [NSIndexPath indexPathForRow:0 inSection:0];
[TableView scrollToRowAtIndexPath:topPath
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
If you are uncertain about the number of rows in your tableView. Use the following:
NSIndexPath *topPath = [NSIndexPath indexPathForRow:NSNotFound inSection:0];
[TableView scrollToRowAtIndexPath:topPath
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
WITHOUT ANIMATION
I'm on iOS11, using a plain tableview style with sticky headers and somehow the only way I got this to work correctly to have the tableview really on top after a reload without any strange flickers/animation behaviours was to use these methods in this order where ALL of these are necessary:
[self.tableView setContentOffset:CGPointZero animated:NO];
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
[self.tableView setContentOffset:CGPointZero animated:NO];
I'm serious that all of these are necessary, even though it seems the first line is not relevant at all. Oh yeah, also very important, do NOT use this:
self.tableView.contentOffset = CGPointZero;
You would think that it's the same as the "setContentOffset animated:FALSE" but apparently it's not! Apple treats this method differently and in my case this only worked when using the full method with animated:FALSE.
WITH ANIMATION
I also wanted to try this with a nice scrolling animation to the top. There the content offset methods seemed to still cause some strange animation behaviours. The only way I got this working with a nice animation after some trial and error were these methods in this exact order:
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:TRUE];
Warning, make sure you have at least 1 row before running the last line of code to avoid crashing :)
I hope this helps someone someday!
This is working for me.
tableView.scrollToRow(at: IndexPath.init(row: 0, section: 0), at: .top, animated: true)
UITableView is a subclass of UIScrollView, so you can also use:
[mainTableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
I was a bit confused by some of the answers I found to this question due to incomplete explanations. From what I've gathered, there's two routes to accomplish what OP requested and they are as follows:
If animation isn't a requirement, I'd suggest reseting scroll position before data loads using one of the offset methods. The reason you should run offset before reloadData is that reloadData runs asynchronously. If you run offset after reload, your scroll position will probably be a few rows down from the top.
tableView.setContentOffset(.zero, animated: false)
tableView.reloadData()
If animation is a requirement, position your scroll to the top row after you run reloadData using scrollToRow. The reason scrollToRow works here is that it positions you at the top of the table, versus the offset method that positions you relative to data that has already loaded.
tableView.reloadData()
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
In swift 3.1
DispatchQueue.main.async(execute: {
self.TableView.reloadData()
self.TableView.contentOffset = .zero
})
For those still finding solution to this, you can use the following extension of UITableView.
Including a check where first section has row(s) or not. It goes till the section with row(s) comes and then scrolls to that one.
extension UITableView {
func scrollToFirst() {
self.reloadData() // if you don't want to reload data, remove this one.
for i in 0..<self.numberOfSections {
if self.numberOfRows(inSection: i) != 0 {
self.scrollToRow(at: IndexPath(row: 0, section: i), at: .top, animated: true)
break
}
}
}
}
Hope that helped you.
If you scroll before reloading and the number of rows decreases, you can have some strange animating behavior. To ensure the scrolling happens after reload, use a method like this:
- (void)reloadTableViewAndScrollToTop:(BOOL)scrollToTop {
[self.tableView reloadData];
if (scrollToTop) {
[self.tableView setContentOffset:CGPointZero animated:YES];
}
}
You must scroll UITableView at the first row of your table view by using this line of code:
self.yourTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
After considering the options discussed above my version of the solutions is the following. It works consistently in iOS 11 & 12 for both UICollectionView & UITableView.
UICollectionView:
DispatchQueue.main.async { [weak self] in
guard self?.<your model object>.first != nil else { return }
self?.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0),
at: .top,
animated: false)
}
UITableView:
DispatchQueue.main.async { [weak self] in
guard self?.<your model object>.first != nil else { return }
self?.collectionView.scrollToRow(at: IndexPath(row: 0, section: 0),
at: .top,
animated: false)
}
Perfectly to the top of page on reload, by twice reloading
dataForSource = nil
tableView.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.dataForSource = realData
self.tableView.reloadData()
}
Xcode 13.2.1 & Swift 5.6
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.contentOffset = .zero

Iphone : How to scroll to the 1st cell of the 2nd section, letting the header of the 1st section visible

I have an UITableView with rows and sections.
I would like to scroll to the first item of the second section, letting the header of the first section visible. Like if I had manually scrolled the list until reaching that state.
---- TOP OF SCREEN ----
Header of first section
Header of the second section
cell 1
cell 2
cell 3
Header of the third section
cell 1
cell 2
...
scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]
does not do the job, it hides the header of the first section.
We're moving on. I found this method based on Kevin's idea. To be able to set animated to YES, I catch the end of animation using a delegate method of UIScrollView. It works. But any solution that would help not doing 2 animations would be greatly appreciated. Any idea about how to do this ?
- (IBAction) scrollToToday:(BOOL)animate {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1] atScrollPosition:UITableViewScrollPositionTop animated:animate];
if (animate == NO) [self showFirstHeaderLine:NO];
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self showFirstHeaderLine:YES];
}
- (void) showFirstHeaderLine:(BOOL)animate {
CGRect headerRect = [self.tableView rectForHeaderInSection:1];
CGPoint scrollPoint = headerRect.origin;
scrollPoint.y -= headerRect.size.height;
[self.tableView setContentOffset:scrollPoint animated:animate];
}
Dude to this code, the process when animated is set to YES should loop infinitely beetween scrollViewDidEndScrollingAnimation and showFirstHeaderLine... It loops, yes, but only once... Any idea about why ?
You can grab the rect for the row you want, then subtract the height of the header of the previous section and scroll to that point. Something like the following (untested) should work:
CGRect rowRect = [table rectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]];
CGRect headerRect = [table rectForHeaderInSection:0];
rowRect.origin.y -= headerRect.size.height;
rowRect.size.height += headerRect.size.height;
[table scrollRectToVisible:rowRect animated:YES]; // UITableView is a subclass of UIScrollView
I tried your code, and it works!!
For the loop question, since you are setting a offset(SetContentOffset), it has nothing to do with the scroll. It is will not call scrollView delegate. SO the scrollViewDidEndScrollingAnimation will be called only once, which has been called from scrollToRowAtIndexPath.

How do I scroll a UITableView to a section that contains no rows?

In an app I'm working on, I have a plain style UITableView that can contain a section containing zero rows. I want to be able to scroll to this section using scrollToRowAtIndexPath:atScrollPosition:animated: but I get an error when I try to scroll to this section due to the lack of child rows.
Apple's calendar application is able to do this, if you look at your calendar in list view, and there are no events in your calendar for today, an empty section is inserted for today and you can scroll to it using the Today button in the toolbar at the bottom of the screen. As far as I can tell Apple may be using a customized UITableView, or they're using a private API...
The only workaround I can think of is to insert an empty UITableCell in that's 0 pixels high and scroll to that. But it's my understanding that having cells of varying heights is really bad for scrolling performance. Still I'll try it anyway, maybe the performance hit won't be too bad.
Update
Since there seems to be no solution to this, I've filed a bug report with apple. If this affects you too, file a duplicate of rdar://problem/6263339 (Open Radar link) if you want this to get this fixed faster.
Update #2
I have a decent workaround to this issue, take a look at my answer below.
UPDATE: Looks like this bug is fixed in iOS 3.0. You can use the following NSIndexPath to scroll to a section containing 0 rows:
[NSIndexPath indexPathForRow:NSNotFound inSection:section]
I'll leave my original workaround here for anyone still maintaining a project using the 2.x SDK.
Found a decent workaround:
CGRect sectionRect = [tableView rectForSection:indexOfSectionToScrollTo];
[tableView scrollRectToVisible:sectionRect animated:YES];
The code above will scroll the tableview so the desired section is visible but not necessarily at the top or bottom of the visible area. If you want to scroll so the section is at the top do this:
CGRect sectionRect = [tableView rectForSection:indexOfSectionToScrollTo];
sectionRect.size.height = tableView.frame.size.height;
[tableView scrollRectToVisible:sectionRect animated:YES];
Modify sectionRect as desired to scroll the desired section to the bottom or middle of the visible area.
If your section have not rows use this
let indexPath = IndexPath(row: NSNotFound, section: section)
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
This is an old question, but Apple still haven't added anything which helps or fixed the crash bug where the section has no rows.
For me, I really needed to make a new section scroll to the middle when added, so I now use this code:
if (rowCount > 0) {
[self.tableView scrollToRowAtIndexPath: [NSIndexPath indexPathForRow: 0 inSection: sectionIndexForNewFolder]
atScrollPosition: UITableViewScrollPositionMiddle
animated: TRUE];
} else {
CGRect sectionRect = [self.tableView rectForSection: sectionIndexForNewFolder];
// Try to get a full-height rect which is centred on the sectionRect
// This produces a very similar effect to UITableViewScrollPositionMiddle.
CGFloat extraHeightToAdd = sectionRect.size.height - self.tableView.frame.size.height;
sectionRect.origin.y -= extraHeightToAdd * 0.5f;
sectionRect.size.height += extraHeightToAdd;
[self.tableView scrollRectToVisible:sectionRect animated:YES];
}
Hope you like it - it's based on Mike Akers' code as you can see, but does the calculation for scrolling to the middle instead of top. Thanks Mike - you're a star.
A Swift approach to the same:
if rows > 0 {
let indexPath = IndexPath(row: 0, section: section)
self.tableView.setContentOffset(CGPoint.zero, animated: true)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
else {
let sectionRect : CGRect = tableView.rect(forSection: section)
tableView.scrollRectToVisible(sectionRect, animated: true)
}
Since using:
[NSIndexPath indexPathForRow:NSNotFound inSection:EXAMPLE]
Broken for me in Xcode 11.3.1 (iOS simulator - 13.3) I decided to use:
NSUInteger index = [self.sectionTypes indexOfObject:#(EXAMPLE)];
if (index != NSNotFound) {
CGRect rect = [self.tableView rectForSection:index];
[self.tableView scrollRectToVisible:rect animated:YES];
}
I think a blank row is probably the only way to go there. Is it possible to redesign the UI such that the "empty" row can display something useful?
I say try it out, and see what the performance is like. They give pretty dire warnings about using transparent sub-views in your list items, and I didn't find that it mattered all that much in my application.