78

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)
rmaddy
  • 314,917
  • 42
  • 532
  • 579
Padmaja
  • 791
  • 1
  • 5
  • 5
  • 1
    When do you call this line. That's of the highest importance. You don't want to call this before the table view has been populated. – M. Porooshani Nov 14 '15 at 05:22
  • Ya i called this line after tableview reloaded – Padmaja Nov 14 '15 at 05:25
  • That's not enough. You see, `reloadData` although seems to be synchronous, acts asynchronously. so you have to either enclose your code in a `dispatchAsync` block on main thread or find another way to do it. I have faced this issue many many times. scrolling to an specific index path is almost futile most of the time. you should use the `setContentOffset` method. – M. Porooshani Nov 14 '15 at 05:29
  • Can u plz tell me, Where i should use This setContentOffset line exactly. – Padmaja Nov 14 '15 at 06:26
  • Possible duplicate of [Automatically scrolling to the bottom of a table with UITableViewAutomaticDimension row height? - Swift, iOS 8+](http://stackoverflow.com/questions/33318021/automatically-scrolling-to-the-bottom-of-a-table-with-uitableviewautomaticdimens) – LaborEtArs May 08 '16 at 18:38
  • This is a question duplicate. Maybe my answer to the other question is helpful to you also: http://stackoverflow.com/a/37099708/2778898 – LaborEtArs May 08 '16 at 18:39

18 Answers18

119

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()
Elijah
  • 8,381
  • 2
  • 55
  • 49
Amit Verma
  • 1,393
  • 1
  • 8
  • 7
  • 8
    Unsafe : ```This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.``` – Jeremie D Feb 26 '17 at 06:13
  • @Jeremie Please add the UI changes to the main queue to avoid this warning – Smit Yash Jun 12 '17 at 08:40
  • I updated the answer to perform this update on the main thread. – Alex Haas Jan 04 '18 at 18:19
  • The TO wants to scroll to the bottom of the UITableView. What if the tableView has a custom contentInset? Your solution only scrolls to the bottom of the last cell. – j3141592653589793238 Aug 03 '18 at 12:01
  • kudos for `tableView.reloadData()` - this was a problem for me that the table didn't know about the number of rows at the point of scrolling – Async- Jul 15 '19 at 08:40
  • @AlexHaas What if the section also gets updated dynamically ? – subin272 Apr 01 '20 at 09:05
  • @subin272 As long as the main thread is aware of the updates, then it should be fine. Do you have a case that doesn't work? – Alex Haas Apr 12 '20 at 23:40
  • Please note that this will crash if your data source has 0 elements, or your table view has no sections. Please see my answer. – John Rogers Oct 08 '20 at 02:01
55

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)
    }
}
Harshil Kotecha
  • 2,846
  • 4
  • 27
  • 41
Umair Afzal
  • 4,947
  • 5
  • 25
  • 50
  • 9
    This will crash if there are 0 rows in the given section. See my answer for an attempted fix. – John Rogers Aug 21 '18 at 00:54
  • your scrollToTop will not scroll to the top if the UITableView has a tableHeaderView. This will work in both cases: setContentOffset(.zero, animated: true) – Elijah Sep 25 '18 at 19:17
  • 1
    I've just added: let indexPath = IndexPath( row: self.numberOfRows(inSection: self.numberOfSections - 1) - 1, section: self.numberOfSections - 1) if (indexPath.row > 0) { self.scrollToRow(at: indexPath, at: .bottom, animated: true) } – Marcelo dos Santos May 28 '19 at 15:46
35

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)
    }
}
John Rogers
  • 2,192
  • 19
  • 29
16

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.

Jayraj Vala
  • 224
  • 2
  • 8
  • This is the perfect solution to scroll to the bottom or any other y position of the table. Great! – Diego Jiménez Oct 27 '21 at 11:10
  • In case your table view has automatic content inset adjustment, use `tableView.adjustedContentInset.bottom` instead of `tableView.contentInset.bottom` to scroll to the exact bottom. – Robin Vekety May 09 '23 at 09:09
9

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.

ipraba
  • 16,485
  • 4
  • 59
  • 58
  • Thank for the solution.When i apply this code works better than before,but still the problem exits.Its not scrolling exactly to the last row. – Padmaja Nov 14 '15 at 06:32
  • When the table view is populated last 5 to 6 cells are left to scroll. – Padmaja Nov 14 '15 at 06:48
  • Try changing the scrollPosition to Top – ipraba Nov 14 '15 at 06:48
  • No change have effected in tableview – Padmaja Nov 14 '15 at 06:56
  • Yes, make sure you do a yourTableview.layoutIfNeeded() before the scrollToRowAtIndexPath is called, this will ensure that if any cell needs automatic resizing, that it is taken care of before the scrolling animation is scheduled. Cheers! – ekscrypto Oct 25 '17 at 12:44
  • The TO wants to scroll to the bottom of the `UITableView`. What if the tableView has a custom contentInset? Your solution only scrolls to the bottom of the last cell. @iPrabu – j3141592653589793238 Aug 03 '18 at 11:59
9

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

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)
    }
}
Matthew Usdin
  • 1,264
  • 1
  • 19
  • 20
5

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)                        
}           
        
João Vitor
  • 241
  • 5
  • 11
  • This crashes when you have 0 items, but it's a good idea. – GJG Oct 07 '20 at 00:16
  • 1
    But what is the point of rolling with 0 items? Is that I use rxswift I only publish values in my structure when the count is different from 0 there works well. – João Vitor Oct 07 '20 at 11:30
2

You can use this one also:-

tableView.scrollRectToVisible(CGRect(x: 0, y: tableView.contentSize.height, width: 1, height: 1), animated: true)
Piyush Naredi
  • 159
  • 1
  • 10
2

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)
            }
        }
    }
}
Ghulam Rasool
  • 3,996
  • 2
  • 27
  • 40
2

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)
}
}
Prakash
  • 812
  • 6
  • 16
  • it works just as it is, however I dont want the animation. if it is set to be false in scrollToRow, it just starts at bottom but glitches and slightly jumps upwards for couple tiny steps – Faruk Aug 08 '21 at 10:14
1

This works in Swift 3.0

let pointsFromTop = CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude)
tableView.setContentOffset(pointsFromTop, animated: true)
silly_cone
  • 137
  • 1
  • 8
1

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)
            }
        }
    }
}
Rahul_Chandnani
  • 275
  • 2
  • 9
0

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)
eemrah
  • 1,603
  • 3
  • 19
  • 37
0

[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()
    }
}
Dmitry Salnikov
  • 337
  • 5
  • 16
0

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)
        }
    }
}
kaevinio
  • 351
  • 1
  • 7
0
tableView.scrollToBottom(animated: true)
-1

Works in Swift 3+ :

        self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentSize.height - UIScreen.main.bounds.height), animated: true)
Dmitrii Z
  • 145
  • 1
  • 5
  • This has several issues. One, the tableview may not take the entire screen, you ignore navigation bars, search bars and any other elements that may be affecting the display area of the table view. You also ignore any content inset that may have been set on the table. – ekscrypto Oct 25 '17 at 12:45