My app has chat functionality and I'm feeding in new messages like this:
[self.tableView beginUpdates];
[messages addObject:msg];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:messages.count - 1 inSection:1]] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:messages.count - 1 inSection:1] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
However, my table view "jumps" weirdly when I'm adding a new message (either sending and receiving, result is the same in both):
Why am I getting this weird "jump"?
OK, I figured it out. As you say, the problem has to do with auto-sizing cells. I used two tricks to make things work (my code is in Swift, but it should be easy to translate back to ObjC):
1) Wait for the table animation to finish before taking further action. This can be done by enclosing the code that updates the table within a block between CATransaction.begin() and CATransaction.commit(). I set the completion block on CATransaction -- that code will run after the animation is finished.
2) Force the table view to render the cell before scrolling to the bottom. I do it by increasing the table's contentOffset by a small amount. That causes the newly inserted cell to get dequeued, and its height gets calculated. Once that scroll is done (I wait for it to finish using the method (1) above), I finally call tableView.scrollToRowAtIndexPath.
Here's the code:
override func viewDidLoad() {
super.viewDidLoad()
// Use auto-sizing for rows
tableView.estimatedRowHeight = 40
tableView.rowHeight = UITableViewAutomaticDimension
tableView.dataSource = self
}
func chatManager(chatManager: ChatManager, didAddMessage message: ChatMessage) {
messages.append(message)
let indexPathToInsert = NSIndexPath(forRow: messages.count-1, inSection: 0)
CATransaction.begin()
CATransaction.setCompletionBlock({ () -> Void in
// This block runs after the animations between CATransaction.begin
// and CATransaction.commit are finished.
self.scrollToLastMessage()
})
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathToInsert], withRowAnimation: .Bottom)
tableView.endUpdates()
CATransaction.commit()
}
func scrollToLastMessage() {
let bottomRow = tableView.numberOfRowsInSection(0) - 1
let bottomMessageIndex = NSIndexPath(forRow: bottomRow, inSection: 0)
guard messages.count > 0
else { return }
CATransaction.begin()
CATransaction.setCompletionBlock({ () -> Void in
// Now we can scroll to the last row!
self.tableView.scrollToRowAtIndexPath(bottomMessageIndex, atScrollPosition: .Bottom, animated: true)
})
// scroll down by 1 point: this causes the newly added cell to be dequeued and rendered.
let contentOffset = tableView.contentOffset.y
let newContentOffset = CGPointMake(0, contentOffset + 1)
tableView.setContentOffset(newContentOffset, animated: true)
CATransaction.commit()
}
Change UITableViewRowAnimationBottom to UITableViewRowAnimationNone and try
Try This!
UITableViewRowAnimation rowAnimation = UITableViewRowAnimationTop;
UITableViewScrollPosition scrollPosition = UITableViewScrollPositionTop;
[self.tableView beginUpdates];
[messages addObject:msg];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:rowAnimation];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:YES];
// Fixes the cell from blinking (because of the transform, when using translucent cells)
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
For Swift 3 and 4
for scroll down to bottom of table View automatically when add new item in the table view just in tableView function add following line its works me
tableView.scrollToRow(at: IndexPath, at: .bottom, animated: true)
for example
in my case I have only one section so 0 is use for section and I have list of orderItems so for last index I use orderItems.count - 1
tableView.scrollToRow(at: [0, orderItems.count - 1], at: .bottom, animated: true)
I've just found out that on ios 11 this problem no longer exists. So there's no longer a content jump when adding a row to a table view and then scrolling to it with scrollToRow(at:) .
Also, on ios 10 calling scrollToRowAtIndexPath with animated=false fixes the content jump
I have a UITableView with cells that are dynamically updated. Everything works fine apart from when tableview.reload is called (see below) to refresh the cells in the table I would like the table to scroll to the bottom to show the new entries.
- (void)reloadTable:(NSNotification *)notification {
NSLog(#"RELOAD TABLE ...");
[customTableView reloadData];
// Scroll to bottom of UITable here ....
}
I was planning to use scrollToRowAtIndexPath:atScrollPosition:animated: but then noticed that I don't have access to an indexPath.
Does anyone know how to do this, or of a delegate callback that I could use?
Use:
NSIndexPath* ipath = [NSIndexPath indexPathForRow: cells_count-1 inSection: sections_count-1];
[tableView scrollToRowAtIndexPath: ipath atScrollPosition: UITableViewScrollPositionTop animated: YES];
Or you can specify the section index manually (If one section => index=0).
Another solution is to flip the table vertically, and flip each cell vertically:
Apply the transform to the UITableView when initializing:
tableview.transform = CGAffineTransformMakeScale(1, -1);
and in cellForRowAtIndexPath:
cell.transform = CGAffineTransformMakeScale(1, -1);
This way you don't need workarounds for scrolling issues, but you will need to think a little harder about contentInsets/contentOffsets and header/footer interactions.
-(void)scrollToBottom{
[self.tableView scrollRectToVisible:CGRectMake(0, self.tableView.contentSize.height - self.tableView.bounds.size.height, self.tableView.bounds.size.width, self.tableView.bounds.size.height) animated:YES];
}
//In swift
var iPath = NSIndexPath(forRow: self.tableView.numberOfRowsInSection(0)-1,
inSection: self.tableView.numberOfSections()-1)
self.tableView.scrollToRowAtIndexPath(iPath,
atScrollPosition: UITableViewScrollPosition.Bottom,
animated: true)
Swift 3
For all the folks here trying to figure out how to solve this problem the key is to call the .layoutIfNeeded() method after .reloadData() :
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentSize.height - tableView.frame.height), animated: false)
I was working with multiple sections in UITableView and it worked well.
Fot Swift 5
extension UITableView {
func scrollToBottom(animated: Bool = true) {
let section = self.numberOfSections
if section > 0 {
let row = self.numberOfRows(inSection: section - 1)
if row > 0 {
self.scrollToRow(at: IndexPath(row: row-1, section: section-1), at: .bottom, animated: animated)
}
}
}
}
As this is something you might want to use really often, I suggest that you create a class extension on UITableView :
extension UITableView {
func scrollToBottom(animated: Bool = true) {
let section = self.numberOfSections
if section > 0 {
let row = self.numberOfRowsInSection(section - 1)
if row > 0 {
self.scrollToRowAtIndexPath(NSIndexPath(forRow: row - 1, inSection: section - 1), atScrollPosition: .Bottom, animated: animated)
}
}
}
}
This is another solution, worked well in my case, when cell height is big.
- (void)scrollToBottom
{
CGPoint bottomOffset = CGPointMake(0, _bubbleTable.contentSize.height - _bubbleTable.bounds.size.height);
if ( bottomOffset.y > 0 ) {
[_bubbleTable setContentOffset:bottomOffset animated:YES];
}
}
extension is better to be done on UIScrollView instead of UITableView, this way it works on scrollView, tableView, collectionView (vertical), UIWebView (inner scroll view), etc
public extension UIScrollView {
public func scrollToBottom(animated animated: Bool) {
let rect = CGRectMake(0, contentSize.height - bounds.size.height, bounds.size.width, bounds.size.height)
scrollRectToVisible(rect, animated: animated)
}
}
Swift 5
func scrollToBottom() {
let section = self.tableView.numberOfSections
let row = self.tableView.numberOfRows(inSection: self.tableView.numberOfSections - 1) - 1;
guard (section > 0) && (row > 0) else{ // check bounds
return
}
let indexPath = IndexPath(row: row-1, section: section-1)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
I dont agree that we should user cells_count,sections_count,self.dateSource.countand so on, Instead, the delegate will be better.
This is the best way.
- (void)scrollToBottom
{
CGFloat yOffset = 0;
if (self.tableView.contentSize.height > self.tableView.bounds.size.height) {
yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height;
}
[self.tableView setContentOffset:CGPointMake(0, yOffset) animated:NO];
}
try this code, It may help you:
self.tableView.reloadData()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.1, execute: {
let indexPath = IndexPath(row: self.dateSource.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: true)
})
What is the proper way of scrolling a UITableView to the top when using estimated cell heights by implementing tableView:estimatedHeightForRowAtIndexPath:?
I noticed that the usual method does not necessarily scroll to the top if there is enough estimation error.
[self.tableView setContentOffset:CGPointMake(0, 0 - self.tableView.contentInset.top) animated:animated];
I came across a similar issue (I wasn't trying to scroll the tableview to the top manually but the view wasn't scrolling correctly when tapping the status bar).
The only way I've come up with to fix this is to ensure in your tableView:estimatedHeightForRowAtIndexPath: method you return the actual height if you know it.
My implementation caches the results of calls to tableView:heightForRowAtIndexPath: for efficiency , so I'm simply looking up this cache in my estimations to see if I already know the real height.
I think the issue comes from tableView:estimatedHeightForRowAtIndexPath: being called in preference over tableView:heightForRowAtIndexPath: even when scrolling upwards over cells that have already been rendered. Just a guess though.
How about tableView.scrollToRow? Solved the issue for me.
Swift 3 example:
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
how about this snippet code
[UIView animateWithDuration:0.0f animations:^{
[_tableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO]; //1
} completion:^(BOOL finished) {
_tableView.contentOffset = CGPointZero; //2
}];
scroll to offset that calculated by estimatedHeightForRowAtIndexPath
setContentOffsetZero
inspiration from https://github.com/caoimghgin/TableViewCellWithAutoLayout/issues/13
let point = { () -> CGPoint in
if #available(iOS 11.0, *) {
return CGPoint(x: -tableView.adjustedContentInset.left, y: -tableView.adjustedContentInset.top)
}
return CGPoint(x: -tableView.contentInset.left, y: -tableView.contentInset.top)
}()
for section in (0..<tableView.numberOfSections) {
if 0 < tableView.numberOfRows(inSection: section) {
// Find the cell at the top and scroll to the corresponding location
tableView.scrollToRow(at: IndexPath(row: 0, section: section),
at: .none,
animated: true)
if tableView.tableHeaderView != nil {
// If tableHeaderView != nil then scroll to the top after the scroll animation ends
CATransaction.setCompletionBlock {
tableView.setContentOffset(point, animated: true)
}
}
return
}
}
tableView.setContentOffset(point, animated: true)
In my table view I have to scroll to the top. But I cannot guarantee that the first object is going to be section 0, row 0. May be that my table view will start from section number 5.
So I get an exception, when I call:
[mainTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
Is there another way to scroll to the top of table view?
UITableView is a subclass of UIScrollView, so you can also use:
[mainTableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
Or
[mainTableView setContentOffset:CGPointZero animated:YES];
And in Swift:
mainTableView.setContentOffset(CGPointZero, animated:true)
And in Swift 3 & above:
mainTableView.setContentOffset(.zero, animated: true)
Note: This answer isn't valid for iOS 11 and later.
I prefer
[mainTableView setContentOffset:CGPointZero animated:YES];
If you have a top inset on your table view, you have to subtract it:
[mainTableView setContentOffset:CGPointMake(0.0f, -mainTableView.contentInset.top) animated:YES];
Possible Actions:
1
func scrollToFirstRow() {
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Top, animated: true)
}
2
func scrollToLastRow() {
let indexPath = NSIndexPath(forRow: objects.count - 1, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
}
3
func scrollToSelectedRow() {
let selectedRows = self.tableView.indexPathsForSelectedRows
if let selectedRow = selectedRows?[0] as? NSIndexPath {
self.tableView.scrollToRowAtIndexPath(selectedRow, atScrollPosition: .Middle, animated: true)
}
}
4
func scrollToHeader() {
self.tableView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: true)
}
5
func scrollToTop(){
self.tableView.setContentOffset(CGPointMake(0, UIApplication.sharedApplication().statusBarFrame.height ), animated: true)
}
Disable Scroll To Top:
func disableScrollsToTopPropertyOnAllSubviewsOf(view: UIView) {
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
(scrollView as UIScrollView).scrollsToTop = false
}
self.disableScrollsToTopPropertyOnAllSubviewsOf(subview as UIView)
}
}
Modify and use it as per requirement.
Swift 4
func scrollToFirstRow() {
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
It's better to not use NSIndexPath (empty table), nor assume that top point is CGPointZero (content insets), that's what I use -
[tableView setContentOffset:CGPointMake(0.0f, -tableView.contentInset.top) animated:YES];
Hope this helps.
Swift 4:
This works very well:
//self.tableView.reloadData() if you want to use this line remember to put it before
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
DONT USE
tableView.setContentOffset(.zero, animated: true)
It can sometimes set the offset improperly. For example, in my case, the cell was actually slightly above the view with safe area insets. Not good.
INSTEAD USE
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
On iOS 11, use adjustedContentInset to correctly scroll to top for both cases when the in-call status bar is visible or not.
if (#available(iOS 11.0, *)) {
[tableView setContentOffset:CGPointMake(0, -tableView.adjustedContentInset.top) animated:YES];
} else {
[tableView setContentOffset:CGPointMake(0, -tableView.contentInset.top) animated:YES];
}
Swift:
if #available(iOS 11.0, *) {
tableView.setContentOffset(CGPoint(x: 0, y: -tableView.adjustedContentInset.top), animated: true)
} else {
tableView.setContentOffset(CGPoint(x: 0, y: -tableView.contentInset.top), animated: true)
}
I've encountered an issue calling trying some of the methods on an empty tableView. Here's another option for Swift 4 that handles empty tableviews.
extension UITableView {
func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
}
func scrollToTop(animated: Bool) {
let indexPath = IndexPath(row: 0, section: 0)
if self.hasRowAtIndexPath(indexPath: indexPath) {
self.scrollToRow(at: indexPath, at: .top, animated: animated)
}
}
}
Usage:
// from yourViewController or yourTableViewController
tableView.scrollToTop(animated: true)//or false
For tables that have a contentInset, setting the content offset to CGPointZero will not work. It'll scroll to the content top vs. scrolling to the table top.
Taking content inset into account produces this instead:
[tableView setContentOffset:CGPointMake(0, -tableView.contentInset.top) animated:NO];
This code let's you scroll a specific section to top
CGRect cellRect = [tableinstance rectForSection:section];
CGPoint origin = [tableinstacne convertPoint:cellRect.origin
fromView:<tableistance>];
[tableinstance setContentOffset:CGPointMake(0, origin.y)];
Swift 5, iOS 13
I know this question already has a lot of answers but from my experience this method always works:
let last = IndexPath(row: someArray.count - 1, section: 0)
tableView.scrollToRow(at: last, at: .bottom, animated: true)
And this is especially true if you're working with animations (like keyboard) or certain async tasks—the other answers will often scroll to the almost bottom. If for some reason this doesn't get you all the way to the bottom, it's almost certainly because of a competing animation so the workaround is to dispatch this animation to the end of the main queue:
DispatchQueue.main.async {
let last = IndexPath(row: self.someArray.count - 1, section: 0)
self.tableView.scrollToRow(at: last, at: .bottom, animated: true)
}
This may seem redundant since you're already on the main queue but it's not because it serializes the animations.
Swift:
tableView.setContentOffset(CGPointZero, animated: true)
Swift 3
tableView.setContentOffset(CGPoint.zero, animated: true)
if tableView.setContentOffset don't work.
Use:
tableView.beginUpdates()
tableView.setContentOffset(CGPoint.zero, animated: true)
tableView.endUpdates()
Since my tableView is full of all kinds of insets, this was the only thing that worked well:
Swift 3
if tableView.numberOfSections > 0 && tableView.numberOfRows(inSection: 0) > 0 {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
Swift 2
if tableView.numberOfSections > 0 && tableView.numberOfRowsInSection(0) > 0 {
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: .Top, animated: true)
}
Adding on to what's already been said, you can create a extension (Swift) or category (Objective C) to make this easier in the future:
Swift:
extension UITableView {
func scrollToTop(animated: Bool) {
setContentOffset(CGPointZero, animated: animated)
}
}
Any time you want to scroll any given tableView to the top you can call the following code:
tableView.scrollToTop(animated: true)
I prefer the following, as it takes into account an inset. If there is no inset, it will still scroll to the top as the inset will be 0.
tableView.setContentOffset(CGPoint(x: 0, y: -tableView.contentInset.top), animated: true)
Swift :
if you don't have tableView header :
tableView.setContentOffset(CGPointMake(0, UIApplication.sharedApplication().statusBarFrame.height ), animated: true)
if so :
tableView.setContentOffset(CGPointMake(0, -tableViewheader.frame.height + UIApplication.sharedApplication().statusBarFrame.height ), animated: true)
In Swift 5 , Thanks #Adrian's answer a lot
extension UITableView{
func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < numberOfSections && indexPath.row < numberOfRows(inSection: indexPath.section)
}
func scrollToTop(_ animated: Bool = false) {
let indexPath = IndexPath(row: 0, section: 0)
if hasRowAtIndexPath(indexPath: indexPath) {
scrollToRow(at: indexPath, at: .top, animated: animated)
}
}
}
Usage:
tableView.scrollToTop()
Here's what I use to work correctly on iOS 11:
extension UIScrollView {
func scrollToTop(animated: Bool) {
var offset = contentOffset
if #available(iOS 11, *) {
offset.y = -adjustedContentInset.top
} else {
offset.y = -contentInset.top
}
setContentOffset(offset, animated: animated)
}
}
using contentOffset is not the right way. this would be better as it is table view's natural way
tableView.scrollToRow(at: NSIndexPath.init(row: 0, section: 0) as IndexPath, at: .top, animated: true)
This was the only code snippet that worked for me
Swift 4:
tableView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: true)
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
tableView.setContentOffset(CGPoint(x: 0, y: -70), animated: true)
P.S. 70 is the height of my header and table view cell
func scrollToTop() {
NSIndexPath *topItem = [NSIndexPath indexPathForItem:0 inSection:0];
[tableView scrollToRowAtIndexPath:topItem atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
call this function wherever you want UITableView scroll to top
Swift 4 via extension, handles empty table view:
extension UITableView {
func scrollToTop(animated: Bool) {
self.setContentOffset(CGPoint.zero, animated: animated);
}
}
I use tabBarController and i have a few section in my tableview at every tab, so this is best solution for me.
extension UITableView {
func scrollToTop(){
for index in 0...numberOfSections - 1 {
if numberOfSections > 0 && numberOfRows(inSection: index) > 0 {
scrollToRow(at: IndexPath(row: 0, section: index), at: .top, animated: true)
break
}
if index == numberOfSections - 1 {
setContentOffset(.zero, animated: true)
break
}
}
}
}
I had to add the multiply by -1 * to the sum of the status bar and the navigation bar, because it was going that height off the screen,
self.tableView.setContentOffset(CGPointMake(0 , -1 *
(self.navigationController!.navigationBar.height +
UIApplication.sharedApplication().statusBarFrame.height) ), animated:true)
in swift
your row = selectioncellRowNumber
your section if you have = selectionNumber if you dont have set is to zero
//UITableViewScrollPosition.Middle or Bottom or Top
var lastIndex = NSIndexPath(forRow: selectioncellRowNumber, inSection: selectionNumber)
self.tableView.scrollToRowAtIndexPath(lastIndex, atScrollPosition: UITableViewScrollPosition.Middle, animated: true)
Here Is The Code To ScrollTableView To Top Programatically
Swift:
self.TableView.setContentOffset(CGPointMake(0, 1), animated:true)
In Swift-3 :
self.tableView.setContentOffset(CGPoint.zero, animated: true)
If you need use Objective-C or you still in love:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
Solution with scrollToRow which fixes the problem for empty TableView (needs for search).
import UIKit
extension UITableView {
public func scrollToTop(animated: Bool = false) {
if numberOfRows(inSection: 0) > 0 {
scrollToRow(
at: .init(row: 0, section: 0),
at: .top,
animated: animated
)
}
}
}