Padmaja
Padmaja

Reputation: 791

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)

Upvotes: 78

Views: 92444

Answers (18)

Pawel Molodkin
Pawel Molodkin

Reputation: 132

tableView.scrollToBottom(animated: true)

Upvotes: 0

Umair Afzal
Umair Afzal

Reputation: 5039

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

Upvotes: 58

Prakash
Prakash

Reputation: 872

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

Upvotes: 3

Ghulam Rasool
Ghulam Rasool

Reputation: 4064

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

Upvotes: 2

kaevinio
kaevinio

Reputation: 520

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

Upvotes: 0

Jo&#227;o Vitor
Jo&#227;o Vitor

Reputation: 240

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

Upvotes: 5

Rahul_Chandnani
Rahul_Chandnani

Reputation: 275

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

Upvotes: 1

Piyush Naredi
Piyush Naredi

Reputation: 171

You can use this one also:-

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

Upvotes: 2

Gangireddy Rami Reddy
Gangireddy Rami Reddy

Reputation: 989

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)

Upvotes: 9

Matthew Usdin
Matthew Usdin

Reputation: 1284

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

Upvotes: 7

Jayraj Vala
Jayraj Vala

Reputation: 224

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.

Upvotes: 16

John Rogers
John Rogers

Reputation: 2192

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

Upvotes: 35

Amit Verma
Amit Verma

Reputation: 1413

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

Upvotes: 121

Dmitry Salnikov
Dmitry Salnikov

Reputation: 336

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

Upvotes: 0

eemmrrkk
eemmrrkk

Reputation: 1700

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)

Upvotes: 0

Dmitrii Z
Dmitrii Z

Reputation: 143

Works in Swift 3+ :

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

Upvotes: -1

silly_cone
silly_cone

Reputation: 157

This works in Swift 3.0

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

Upvotes: 1

ipraba
ipraba

Reputation: 16543

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.

Upvotes: 9

Related Questions