acqu13sce
acqu13sce

Reputation: 3809

How to scroll to the bottom of a UITableView on the iPhone before the view appears

I have a UITableView that is populated with cells of a variable height. I would like the table to scroll to the bottom when the view is pushed into view.

I currently have the following function

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[log count]-1 inSection:0];
[self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];

log is a mutable array containing the objects that make up the content of each cell.

The above code works fine in viewDidAppear however this has the unfortunate side effect of displaying the top of the table when the view first appears and then jumping to the bottom. I would prefer it if the table view could be scrolled to the bottom before it appears.

I tried the scroll in viewWillAppear and viewDidLoad but in both cases the data has not been loaded into the table yet and both throw an exception.

Any guidance would be much appreciated, even if it's just a case of telling me what I have is all that is possible.

Upvotes: 147

Views: 186615

Answers (30)

Muhammad Asher
Muhammad Asher

Reputation: 1

Swift 5 Here is a simple way to solve this problem

Steps:-> 1- When ViewDidLoad(), Then it will scroll 2- customTableView is IBoutlet's tableView var data: [String] = ["Hello", "This","is","Your","World"]

   override func viewDidLoad() {
   super.viewDidLoad()
  
3- In viewDidLoad we need to tell about indexPath
   /// data.count-1 will give last cell 
   let indexPath = IndexPath(row: data.count - 1, section:0)

   /// 1st Parameter: (at) will use for indexPath
   /// 2nd Parameter: (at) will use for position's scroll view
   /// 3rd Parameter: (animated) will show animation when screen will appear

   customtableView.scrollToRow(at:indexPath, at:.top, animated:true)
    /// Reload CustomTableView
   customTableView.reloadData()

Upvotes: 0

Mike Keskinov
Mike Keskinov

Reputation: 11878

In one line:

tableView.scrollToRow(at: IndexPath(row: data.count - 1, section: 0), at: .bottom, animated: true)

This code is IMHO more clear than the accepted answer.

Upvotes: 4

Jacob Relkin
Jacob Relkin

Reputation: 163248

I believe that calling

 tableView.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)

will do what you want.

Upvotes: 154

Egzon P.
Egzon P.

Reputation: 4778

The safest way to scroll at the bottom of tableView is to use "tableView.scrollRectToVisible". Use after calling tableView.reloadData(). In Swift 5

private func scrollAtTheBottom() {
    let lastIndexPath = IndexPath(row: state.myItemsArray.count - 1, section: 0)
    let lastCellPosition = tableView.rectForRow(at: lastIndexPath)
    tableView.scrollRectToVisible(lastCellPosition, animated: true)
}

Upvotes: 0

Rate Morgan
Rate Morgan

Reputation: 305

I found another good way, as follows:

func yourFunc() {
    //tableView.reloadData()
    self.perform(#selector(scrollToBottom), with: nil, afterDelay: 0.1)
}

@objc func scrollToBottom() {
    self.tableView.scrollToRow(at: IndexPath(row: 0, section: self.mesArr.count - 1), at: .bottom, animated: false)
}

Upvotes: 0

Mahesh
Mahesh

Reputation: 2757

Thanks Jacob for the answer. really helpfull if anyone interesting with monotouch c# version

private void SetScrollPositionDown() {
    if (tblShoppingListItem.ContentSize.Height > tblShoppingListItem.Frame.Size.Height) {
        PointF offset = new PointF(0, tblShoppingListItem.ContentSize.Height - tblShoppingListItem.Frame.Size.Height);
        tblShoppingListItem.SetContentOffset(offset,true );
    }
}

Upvotes: 2

XcodeNOOB
XcodeNOOB

Reputation: 2185

Actually a "Swifter" way to do it in swift is :

var lastIndex = NSIndexPath(forRow: self.messages.count - 1, inSection: 0)
self.messageTableView.scrollToRowAtIndexPath(lastIndex, atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)

work Perfect for me.

Upvotes: 17

Segev
Segev

Reputation: 1287

For Swift:

if tableView.contentSize.height > tableView.frame.size.height {
    let offset = CGPoint(x: 0, y: tableView.contentSize.height - tableView.frame.size.height)
    tableView.setContentOffset(offset, animated: false)
}

Upvotes: 9

Bibin Joseph
Bibin Joseph

Reputation: 234

Use this simple code to scroll tableView bottom

NSInteger rows = [tableName numberOfRowsInSection:0];
if(rows > 0) {
    [tableName scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:rows-1 inSection:0]
                     atScrollPosition:UITableViewScrollPositionBottom
                             animated:YES];
}

Upvotes: 3

gkaimakas
gkaimakas

Reputation: 582

On iOS 12 the accepted answer seems to be broken. I had it working with the following extension



import UIKit

extension UITableView {
    public func scrollToBottom(animated: Bool = true) {
        guard let dataSource = dataSource else {
            return
        }

        let sections = dataSource.numberOfSections?(in: self) ?? 1
        let rows = dataSource.tableView(self, numberOfRowsInSection: sections-1)
        let bottomIndex = IndexPath(item: rows - 1, section: sections - 1)

        scrollToRow(at: bottomIndex,
                    at: .bottom,
                    animated: animated)
    }
}

Upvotes: 1

Vasily  Bodnarchuk
Vasily Bodnarchuk

Reputation: 25294

Details

  • Xcode 8.3.2, swift 3.1
  • Xcode 10.2 (10E125), Swift 5

Code

import UIKit

extension UITableView {
    func scrollToBottom(animated: Bool) {
        let y = contentSize.height - frame.size.height
        if y < 0 { return }
        setContentOffset(CGPoint(x: 0, y: y), animated: animated)
    }
}

Usage

tableView.scrollToBottom(animated: true)

Full sample

Do not forget to paste solution code!

import UIKit

class ViewController: UIViewController {

    private weak var tableView: UITableView?
    private lazy var cellReuseIdentifier = "CellReuseIdentifier"

    override func viewDidLoad() {
        super.viewDidLoad()
        let tableView = UITableView(frame: view.frame)
        view.addSubview(tableView)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)
        self.tableView = tableView
        tableView.dataSource = self
        tableView.performBatchUpdates(nil) { [weak self] result in
            if result { self?.tableView?.scrollToBottom(animated: true) }
        }
    }
}

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 100
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath )
        cell.textLabel?.text = "\(indexPath)"
        return cell
    }
}

Upvotes: 15

Chamira Fernando
Chamira Fernando

Reputation: 6846

I think the easiest way is this:

if (self.messages.count > 0)
{
    [self.tableView 
        scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.messages.count-1 
        inSection:0] 
        atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

Swift 3 Version:

if messages.count > 0 {
    userDefinedOptionsTableView.scrollToRow(at: IndexPath(item:messages.count-1, section: 0), at: .bottom, animated: true)
}

Upvotes: 128

Rafał Sroka
Rafał Sroka

Reputation: 40030

I'm using autolayout and none of the answers worked for me. Here is my solution that finally worked:

@property (nonatomic, assign) BOOL shouldScrollToLastRow;


- (void)viewDidLoad {
    [super viewDidLoad];

    _shouldScrollToLastRow = YES;
}


- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    // Scroll table view to the last row
    if (_shouldScrollToLastRow)
    {
        _shouldScrollToLastRow = NO;
        [self.tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
    }
}

Upvotes: 31

ymutlu
ymutlu

Reputation: 6715

I believe old solutions do not work with swift3.

If you know number rows in table you can use :

tableView.scrollToRow(
    at: IndexPath(item: listCountInSection-1, section: sectionCount - 1 ), 
    at: .top, 
    animated: true)

Upvotes: 0

Osama F Elias
Osama F Elias

Reputation: 1564

From Jacob's answer, this is the code:

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (self.messagesTableView.contentSize.height > self.messagesTableView.frame.size.height) 
    {
        CGPoint offset = CGPointMake(0, self.messagesTableView.contentSize.height - self.messagesTableView.frame.size.height);
        [self.messagesTableView setContentOffset:offset animated:YES];
    }
}

Upvotes: 121

user515060
user515060

Reputation:

func scrollToBottom() {

    let sections = self.chatTableView.numberOfSections

    if sections > 0 {

        let rows = self.chatTableView.numberOfRows(inSection: sections - 1)

        let last = IndexPath(row: rows - 1, section: sections - 1)

        DispatchQueue.main.async {

            self.chatTableView.scrollToRow(at: last, at: .bottom, animated: false)
        }
    }
}

you should add

DispatchQueue.main.async {
            self.chatTableView.scrollToRow(at: last, at: .bottom, animated: false)
        }

or it will not scroll to bottom.

Upvotes: 4

Tarik
Tarik

Reputation: 539

Function on swift 3 scroll to bottom

 override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(false)
        //scroll down
        if lists.count > 2 {
            let numberOfSections = self.tableView.numberOfSections
            let numberOfRows = self.tableView.numberOfRows(inSection: numberOfSections-1)
            let indexPath = IndexPath(row: numberOfRows-1 , section: numberOfSections-1)
            self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.middle, animated: true)
        }
    }

Upvotes: 2

Amit Verma
Amit Verma

Reputation: 1413

In swift 3.0 If you want to go any particular Cell of tableview Change cell index Value like change "self.yourArr.count" value .

self.yourTable.reloadData()
self.scrollToBottom() 
func scrollToBottom(){
    DispatchQueue.global(qos: .background).async {
        let indexPath = IndexPath(row: self.yourArr.count-1, section: 0)
        self.tblComment.scrollToRow(at: indexPath, at: .bottom, animated: true)
    }
}

Upvotes: 0

Amit Verma
Amit Verma

Reputation: 1413

In Swift 3.0

self.tableViewFeeds.setContentOffset(CGPoint(x: 0, y: CGFLOAT_MAX), animated: true)

Upvotes: 3

Irshad Qureshi
Irshad Qureshi

Reputation: 819

For Swift 3 ( Xcode 8.1 ):

override func viewDidAppear(_ animated: Bool) {
    let numberOfSections = self.tableView.numberOfSections
    let numberOfRows = self.tableView.numberOfRows(inSection: numberOfSections-1)

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

Upvotes: 5

SoftDesigner
SoftDesigner

Reputation: 5853

If you have to load the data asynchronously prior to scrolling down, here's the possible solution:

tableView.alpha = 0 // We want animation!
lastMessageShown = false // This is ivar

viewModel.fetch { [unowned self] result in
    self.tableView.reloadData()

    if !self.lastMessageShown {
        dispatch_async(dispatch_get_main_queue()) { [unowned self] in
            if self.rowCount > 0 {
                self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: self.rowCount, inSection: 0), atScrollPosition: .Bottom, animated: false)
            }

            UIView.animateWithDuration(0.1) {
                self.tableView.alpha = 1
                self.lastMessageShown = true // Do it once
            }
        }
    }
}

Upvotes: 2

Even Cheng
Even Cheng

Reputation: 1996

In Swift, you just need

self.tableView.scrollToNearestSelectedRowAtScrollPosition(UITableViewScrollPosition.Bottom, animated: true)

to make it automatically scroll to the buttom

Upvotes: -2

Narasimha Nallamsetty
Narasimha Nallamsetty

Reputation: 1263

If you are setting up frame for tableview programmatically, make sure you are setting frame correctly.

Upvotes: 1

Ryan Herubin
Ryan Herubin

Reputation: 620

Here's an extension that I implemented in Swift 2.0. These functions should be called after the tableview has been loaded:

import UIKit

extension UITableView {
    func setOffsetToBottom(animated: Bool) {
        self.setContentOffset(CGPointMake(0, self.contentSize.height - self.frame.size.height), animated: true)
    }

    func scrollToLastRow(animated: Bool) {
        if self.numberOfRowsInSection(0) > 0 {
            self.scrollToRowAtIndexPath(NSIndexPath(forRow: self.numberOfRowsInSection(0) - 1, inSection: 0), atScrollPosition: .Bottom, animated: animated)
        }
    }
}

Upvotes: 24

skot
skot

Reputation: 598

After a lot of fiddling this is what worked for me:

var viewHasAppeared = false

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if !viewHasAppeared { goToBottom() }
}

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    viewHasAppeared = true
}

private func goToBottom() {
    guard data.count > 0 else { return }
    let indexPath = NSIndexPath(forRow: data.count - 1, inSection: 0)
    tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
    tableView.layoutIfNeeded()
}

The key turned out to be not wrapping scrollToRowAtIndexPath inside of dispatch_async as some have suggested, but simply following it with a call to layoutIfNeeded.

My understanding of this is, calling the scroll method in the current thread guarantees that the scroll offset is set immediately, before the view is displayed. When I was dispatching to the main thread, the view was getting displayed for an instant before the scroll took effect.

(Also NB you need the viewHasAppeared flag because you don't want to goToBottom every time viewDidLayoutSubviews is called. It gets called for example whenever the orientation changes.)

Upvotes: 4

Anuj Kumar Rai
Anuj Kumar Rai

Reputation: 666

No need for any scrolling you can just do it by using this code:

[YOURTABLEVIEWNAME setContentOffset:CGPointMake(0, CGFLOAT_MAX)];

Upvotes: 1

user3246173
user3246173

Reputation: 488

The accepted answer didn't work with my table (thousands of rows, dynamic loading) but the code below works:

- (void)scrollToBottom:(id)sender {
    if ([self.sections count] > 0) {
        NSInteger idx = [self.sections count] - 1;
        CGRect sectionRect = [self.tableView rectForSection:idx];
        sectionRect.size.height = self.tableView.frame.size.height;
        [self.tableView scrollRectToVisible:sectionRect animated:NO];
    }
}

Upvotes: 1

leetvin
leetvin

Reputation: 21

[self.tableViewInfo scrollRectToVisible:CGRectMake(0, self.tableViewInfo.contentSize.height-self.tableViewInfo.height, self.tableViewInfo.width, self.tableViewInfo.height) animated:YES];

Upvotes: 1

Mohsen Karbassi
Mohsen Karbassi

Reputation: 71

Is of course a Bug. Probably somewhere in your code you use table.estimatedRowHeight = value (for example 100). Replace this value by the highest value you think a row height could get, for example 500.. This should solve the problem in combination with following code:

//auto scroll down example
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))

dispatch_after(time, dispatch_get_main_queue(), {
    self.table.scrollToRowAtIndexPath(NSIndexPath(forRow: self.Messages.count - 1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Bottom, animated: false)
})

Upvotes: 4

Josip B.
Josip B.

Reputation: 2464

In iOS this worked fine for me

CGFloat height = self.inputTableView.contentSize.height;
if (height > CGRectGetHeight(self.inputTableView.frame)) {
    height -= (CGRectGetHeight(self.inputTableView.frame) - CGRectGetHeight(self.navigationController.navigationBar.frame));
}
else {
    height = 0;
}
[self.inputTableView setContentOffset:CGPointMake(0, height) animated:animated];

It needs to be called from viewDidLayoutSubviews

Upvotes: 1

Related Questions