How to scroll to the exact end of the UITableView?

I have a UITableView that is populated with cells with dynamic height. I would like the table to scroll to the bottom when the view controller is pushed from view controller.

I have tried with contentOffset and tableView scrollToRowAtIndexPath but still I am not getting the perfect solution for exactly I want.

Can anyone please help me fix this issue?

Here is my code to scroll:

let indexPath = NSIndexPath(forRow: commentArray.count-1, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
80250 次浏览

When you push the viewcontroller having the tableview you should scrollTo the specified indexPath only after your Tableview is finished reloading.

yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let indexPath = NSIndexPath(forRow: commentArray.count-1, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)


})

The reason for putting the method inside the dispatch_async is once you execute reloadData the next line will get executed immediately and then reloading will happen in main thread. So to know when the tableview gets finished(After all cellforrowindex is finished) we use GCD here. Basically there is no delegate in tableview will tell that the tableview has finished reloading.

For Swift 3.0

Write a function :

func scrollToBottom(){
DispatchQueue.main.async {
let indexPath = IndexPath(row: self.dataArray.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}

and call it after you reload the tableview data

tableView.reloadData()
scrollToBottom()

This works in Swift 3.0

let pointsFromTop = CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude)
tableView.setContentOffset(pointsFromTop, animated: true)

Works in Swift 3+ :

        self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentSize.height - UIScreen.main.bounds.height), animated: true)

If you used UINavigationBar with some height and UITableView in the UIView try it to subtract UINavigationBar height from UITableView's frame height . Cause your UITableView's top point same with UINavigationBar's bottom point so this affect your UITableView's bottom items to scrolling.

Swift 3

simpleTableView.frame = CGRect.init(x: 0, y: navigationBarHeight, width: Int(view.frame.width), height: Int(view.frame.height)-navigationBarHeight)

[Swift 3, iOS 10]

I've ended up using kind-of-hacky solution, but it doesn't depend on rows indexpaths (which leads to crashes sometimes), cells dynamic height or table reload event, so it seems pretty universal and in practice works more reliable than others I've found.

  • use KVO to track table's contentOffset

  • fire scroll event inside KVO observer

  • schedule scroll invocation using delayed Timer to filter multiple
    observer triggers

The code inside some ViewController:

private var scrollTimer: Timer?
private var ObserveContext: Int = 0


override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
table.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: &ObserveContext)
}


override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
table.removeObserver(self, forKeyPath: "contentSize")
}


override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (context == &ObserveContext) {
self.scheduleScrollToBottom()
}
}


func scheduleScrollToBottom() {


if (self.scrollTimer == nil) {
self.scrollTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] (timer) in
let table = self!.table


let bottomOffset = table.contentSize.height - table.bounds.size.height
if !table.isDragging && bottomOffset > 0 {
let point: CGPoint = CGPoint(x: 0, y: bottomOffset)
table.setContentOffset(point, animated: true)
}


timer.invalidate()
self?.scrollTimer = nil
})
self.scrollTimer?.fire()
}
}

I would use more generic approach to this:

Swift4

extension UITableView {


func scrollToBottom(){


DispatchQueue.main.async {
let indexPath = IndexPath(
row: self.numberOfRows(inSection:  self.numberOfSections-1) - 1,
section: self.numberOfSections - 1)
if hasRowAtIndexPath(indexPath) {
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
}


func scrollToTop() {


DispatchQueue.main.async {
let indexPath = IndexPath(row: 0, section: 0)
if hasRowAtIndexPath(indexPath) {
self.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}


func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
}
}

Swift5

extension UITableView {


func scrollToBottom(isAnimated:Bool = true){


DispatchQueue.main.async {
let indexPath = IndexPath(
row: self.numberOfRows(inSection:  self.numberOfSections-1) - 1,
section: self.numberOfSections - 1)
if self.hasRowAtIndexPath(indexPath: indexPath) {
self.scrollToRow(at: indexPath, at: .bottom, animated: isAnimated)
}
}
}


func scrollToTop(isAnimated:Bool = true) {


DispatchQueue.main.async {
let indexPath = IndexPath(row: 0, section: 0)
if self.hasRowAtIndexPath(indexPath: indexPath) {
self.scrollToRow(at: indexPath, at: .top, animated: isAnimated)
}
}
}


func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
}
}

I tried Umair's approach, however in UITableViews, sometimes there can be a section with 0 rows; in which case, the code points to an invalid index path (row 0 of an empty section is not a row).

Blindly minusing 1 from the number of rows/sections can be another pain point, as, again, the row/section could contain 0 elements.

Here's my solution to scrolling to the bottom-most cell, ensuring the index path is valid:

extension UITableView {
func scrollToBottomRow() {
DispatchQueue.main.async {
guard self.numberOfSections > 0 else { return }


// Make an attempt to use the bottom-most section with at least one row
var section = max(self.numberOfSections - 1, 0)
var row = max(self.numberOfRows(inSection: section) - 1, 0)
var indexPath = IndexPath(row: row, section: section)


// Ensure the index path is valid, otherwise use the section above (sections can
// contain 0 rows which leads to an invalid index path)
while !self.indexPathIsValid(indexPath) {
section = max(section - 1, 0)
row = max(self.numberOfRows(inSection: section) - 1, 0)
indexPath = IndexPath(row: row, section: section)


// If we're down to the last section, attempt to use the first row
if indexPath.section == 0 {
indexPath = IndexPath(row: 0, section: 0)
break
}
}


// In the case that [0, 0] is valid (perhaps no data source?), ensure we don't encounter an
// exception here
guard self.indexPathIsValid(indexPath) else { return }


self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}


func indexPathIsValid(_ indexPath: IndexPath) -> Bool {
let section = indexPath.section
let row = indexPath.row
return section < self.numberOfSections && row < self.numberOfRows(inSection: section)
}
}

For perfect scroll to bottom solution use tableView contentOffset

func scrollToBottom()  {
let point = CGPoint(x: 0, y: self.tableView.contentSize.height + self.tableView.contentInset.bottom - self.tableView.frame.height)
if point.y >= 0{
self.tableView.setContentOffset(point, animated: animate)
}
}
Performing scroll to bottom in main queue works bcoz it is delaying the execution and result in working since after loading of viewController and delaying through main queue tableView now knows its content size.

I rather use self.view.layoutIfNeeded() after filling my data onto tableView and then call my method scrollToBottom(). This works fine for me.

A little update of @Umair answer in case your tableView is empty

func scrollToBottom(animated: Bool = true, delay: Double = 0.0) {
let numberOfRows = tableView.numberOfRows(inSection: tableView.numberOfSections - 1) - 1
guard numberOfRows > 0 else { return }


DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [unowned self] in


let indexPath = IndexPath(
row: numberOfRows,
section: self.tableView.numberOfSections - 1)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: animated)
}
}

Works in Swift 4+ :

   self.tableView.reloadData()
let indexPath = NSIndexPath(row: self.yourDataArray.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath as IndexPath, at: .bottom, animated: true)

This is a function that contains the completion closer:

func reloadAndScrollToTop(completion: @escaping () -> Void) {
self.tableView.reload()
completion()
}

And use:

self.reloadAndScrollToTop(completion: {
tableView.scrollToRow(at: indexPath, at: .top, animated: true))
})

The tableView.scrollTo ... line will be executed after all table cells have been safely loaded.

You can use this one also:-

tableView.scrollRectToVisible(CGRect(x: 0, y: tableView.contentSize.height, width: 1, height: 1), animated: true)

Best way to scroll scrollToBottom:

before call scrollToBottom method call following method

self.view.layoutIfNeeded()
extension UITableView {
func scrollToBottom(animated:Bool)  {
let numberOfRows = self.numberOfRows(inSection: self.numberOfSections - 1) - 1
if numberOfRows >= 0{
let indexPath = IndexPath(
row: numberOfRows,
section: self.numberOfSections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: animated)
} else {
let point = CGPoint(x: 0, y: self.contentSize.height + self.contentInset.bottom - self.frame.height)
if point.y >= 0{
self.setContentOffset(point, animated: animated)
}
}
}
}

Using Swift 5 you can also do this after each reload:

DispatchQueue.main.async {
let index = IndexPath(row: self.itens.count-1, section: 0)
self.tableView.scrollToRow(at: index, at: .bottom, animated: true)
}
        

To scroll to the end of your TableView you can use the following function, which also works for ScrollViews.

It also calculates the safe area on the bottom for iPhone X and newer. The call is made from the main queue, to calculate the height correctly.

func scrollToBottom(animated: Bool) {
DispatchQueue.main.async {
let bottomOffset = CGPoint(x: 0, y: self.contentSize.height - self.bounds.size.height + self.safeAreaBottom)
        

if bottomOffset.y > 0 {
self.setContentOffset(bottomOffset, animated: animated)
}
}
}

For Swift 5 or higher version

import UIKit


extension UITableView {
    

func scrollToBottom(animated: Bool) {
        

DispatchQueue.main.async {
let point = CGPoint(x: 0, y: self.contentSize.height + self.contentInset.bottom - self.frame.height)
if point.y >= 0 {
self.setContentOffset(point, animated: animated)
}
}
}
}

Swift 5 solution

extension UITableView {
func scrollToBottom(){


DispatchQueue.main.async {
let indexPath = IndexPath(
row: self.numberOfRows(inSection:  self.numberOfSections-1) - 1,
section: self.numberOfSections - 1)
if self.hasRowAtIndexPath(indexPath: indexPath) {
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
}


func scrollToTop() {
DispatchQueue.main.async {
let indexPath = IndexPath(row: 0, section: 0)
if self.hasRowAtIndexPath(indexPath: indexPath) {
self.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}


func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
}
}