ChatGPT解决这个技术问题 Extra ChatGPT

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.


S
Sourabh Sharma

I believe that calling

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

will do what you want.


That's perfect thank you. I created a CGPoint with a sufficiently high Y value that will make it always display the bottom. Once the view has loaded I can use (self.table.contentSize.height - self.table.frame.size.height) to move to the bottom with the same method
Though this is perfect answer, as we don't need to do calculation for how many cells, height of tableview etc.. BUT I would point that we need to call this before reloading the tableview... It won't work if we write this after [table reloadData];
doesn't work on ios 10-12 - table simply disappear for first time
Or just scroll to CGPoint(x: 0, y: tableView.contentSize.height) ?
Yikes!!! Makes the table vanish :-o. Best using [self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
M
Mars

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

It doesn't work if the cell is taller than the UITableView
Cell is Taller than UITableView? never heard such a use case.
@ChamiraFernando this is the easiest way :)
Note that it might make sense to replace the messages.count by the already implemented myTableView.dataSource!.tableView(myTableView, numberOfRowsInSection: 0). Yes it's longer, but it might avoid code repetition. You also need to handle the optional dataSource (don't force unwrap as in this sample).
@ChamiraFernando I know this question is old, but just because you never saw, it does not mean it doesn't happens. To answer your question, apps like Foursquare may have this situation, where user writes a review. The cell's height is great than the height of the tableview. Its a perfectly fine situation.
C
Community

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];
    }
}

On iOS 11 you should use adjusted table view frame height: UIEdgeInsetsInsetRect(self.messagesTableView.frame, self.messagesTableView.safeAreaInsets).height
g
gerry3

If you need to scroll to the EXACT end of the content, you can do it like this:

- (void)scrollToBottom
{
    CGFloat yOffset = 0;

    if (self.tableView.contentSize.height > self.tableView.bounds.size.height) {
        yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height;
    }

    [self.tableView setContentOffset:CGPointMake(0, yOffset) animated:NO];
}

Works with autolayout, BUT it's important to call this method from viewDidLayoutSubviews
Could you please explain why we need to do this: yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height; ? Thanks.
@Unheilig If you would scroll to self.tableView.contentSize.height the content of the table view may be not visible, because you scroll below the content. Therefore you have to scroll to one "visible table view gap" above the end of the table view.
R
Rafa de King

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

This almost works for me but I get a weird graphical glitch whilst my table data is loading from an external API. In my case do I need to call setContentOffset at some other point when the data has been fetched and tableview reloaded?
Try setting the offset in a completion handler of your request.
This does not work in ios 10 - simply shows a table with a black background
Instead of using CGFLOAT_MAX I used contentSize.height - frame.height + contentInset.bottom when setting the initial content offset. Using CGFLOAT_MAX seemed to mess up for me.
R
Ryan Herubin

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

This is better than using content size. For Swift3. if self.numberOfRows(inSection: 0) > 0 { self.scrollToRow(at: IndexPath.init(row: self.numberOfRows(inSection: 0)-1, section: 0), at: .bottom, animated: animated) }
if scrollToLastRow this method is not work perfect then just add self.layoutIfNeeded() and it's work perfect !!
C
Community

The accepted solution by @JacobRelkin didn't work for me in iOS 7.0 using Auto Layout.

I have a custom subclass of UIViewController and added an instance variable _tableView as a subview of its view. I positioned _tableView using Auto Layout. I tried calling this method at the end of viewDidLoad and even in viewWillAppear:. Neither worked.

So, I added the following method to my custom subclass of UIViewController.

- (void)tableViewScrollToBottomAnimated:(BOOL)animated {
    NSInteger numberOfRows = [_tableView numberOfRowsInSection:0];
    if (numberOfRows) {
        [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:numberOfRows-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
    }
}

Calling [self tableViewScrollToBottomAnimated:NO] at the end of viewDidLoad works. Unfortunately, it also causes tableView:heightForRowAtIndexPath: to get called three times for every cell.


O
Olcay Ertaş

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.


V
Vasily Bodnarchuk

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

T
T.J.

I wanted the table to load with the end of the table shown in the frame. I found using

NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:([self.tableView numberOfRowsInSection:0] - 1) inSection:0];
[[self tableView] scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];

did not work because it gave an error when table height was less than the frame height. Note my table only has one section.

The solution that worked for me was implement the following code in viewWillAppear:

- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// on the initial cell load scroll to the last row (ie the latest Note)
if (initialLoad==TRUE) {
    initialLoad=FALSE; 
    NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:([self.tableView numberOfRowsInSection:0] - 1) inSection:0];
    [[self tableView] scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
        CGPoint offset = CGPointMake(0, (1000000.0));
        [self.tableView setContentOffset:offset animated:NO];
    }
}

The BOOL ivar initialLoad is set to TRUE in viewDidLoad.


Do you need to call scrollToRowAtIndexPath at all? You're already calling setContentOffset afterwards, which might make that first call pointless.
O
Olcay Ertaş

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

E
Erphan Rajput

You should use UITableViewScrollPositionBottom instead.


I
Irshad Qureshi

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

This doesn't answer OP question, this is what was working from the start. Also you should call super.viewDidAppear
M
Machavity

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

s
skot

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


J
JimmyJammed

Using the above solutions, this will scroll to the bottom of your table (only if the table content is loaded first):

//Scroll to bottom of table
CGSize tableSize = myTableView.contentSize;
[myTableView setContentOffset:CGPointMake(0, tableSize.height)];

That's right. You can't use this solution while you need to update your table.
M
Matthew Verstraete

In Swift 3.0

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

佚名
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.


S
SoftDesigner

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

T
Tarik

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

O
Olcay Ertaş

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];
}

This is basically what the OP said he already tried. This answer does not address the OP's question of how to get it to work properly in viewWillAppear.
O
Olcay Ertaş

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

M
Mike Keskinov

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.


E
Eran Goldin

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


l
leetvin

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


u
user3246173

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];
    }
}

P
Paul Roub

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

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

N
Narasimha Nallamsetty

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


g
gkaimakas

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

A
Amit Verma

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

关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now