How to scroll to the exact end of the UITableView?

IosSwiftUitableview

Ios Problem Overview


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)

Ios Solutions


Solution 1 - Ios

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()

Solution 2 - Ios

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)
    }
}

Solution 3 - Ios

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)
    }
}

Solution 4 - Ios

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.

Solution 5 - Ios

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.

Solution 6 - Ios

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)

Solution 7 - Ios

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)
    }
}

Solution 8 - Ios

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)                        
}           
        

Solution 9 - Ios

You can use this one also:-

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

Solution 10 - Ios

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)
            }
        }
    }
}

Solution 11 - Ios

This works in Swift 3.0

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

Solution 12 - Ios

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)
            }
        }
    }
}

Solution 13 - Ios

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)
}
}

Solution 14 - Ios

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)

Solution 15 - Ios

[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()
    }
}

Solution 16 - Ios

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)
        }
    }
}

Solution 17 - Ios

Works in Swift 3+ :

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

Solution 18 - Ios

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPadmajaView Question on Stackoverflow
Solution 1 - IosAmit VermaView Answer on Stackoverflow
Solution 2 - IosUmair AfzalView Answer on Stackoverflow
Solution 3 - IosJohn RogersView Answer on Stackoverflow
Solution 4 - IosJayraj ValaView Answer on Stackoverflow
Solution 5 - IosiprabaView Answer on Stackoverflow
Solution 6 - IosGangireddy Rami ReddyView Answer on Stackoverflow
Solution 7 - IosmattyUView Answer on Stackoverflow
Solution 8 - IosJoão VitorView Answer on Stackoverflow
Solution 9 - IosPiyush NarediView Answer on Stackoverflow
Solution 10 - IosGhulam RasoolView Answer on Stackoverflow
Solution 11 - Iossilly_coneView Answer on Stackoverflow
Solution 12 - IosRahul_ChandnaniView Answer on Stackoverflow
Solution 13 - IosPrakashView Answer on Stackoverflow
Solution 14 - IoseemrahView Answer on Stackoverflow
Solution 15 - IosDmitry SalnikovView Answer on Stackoverflow
Solution 16 - IosKevin WaltzView Answer on Stackoverflow
Solution 17 - IosDmitrii ZView Answer on Stackoverflow
Solution 18 - IosEido 9oyaView Answer on Stackoverflow