How to scroll to the bottom of a UITableView on the iPhone before the view appears
IosObjective CCocoa TouchUitableviewIos Problem Overview
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.
Ios Solutions
Solution 1 - Ios
I believe that calling
tableView.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)
will do what you want.
Solution 2 - Ios
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)
}
Solution 3 - Ios
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];
}
}
Solution 4 - Ios
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];
}
Solution 5 - Ios
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)];
}
}
Solution 6 - Ios
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)
}
}
}
Solution 7 - Ios
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.
Solution 8 - Ios
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.
Solution 9 - Ios
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
}
}
Solution 10 - Ios
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.
Solution 11 - Ios
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)
}
Solution 12 - Ios
You should use UITableViewScrollPositionBottom
instead.
Solution 13 - Ios
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)
}
Solution 14 - Ios
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)
})
Solution 15 - Ios
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.)
Solution 16 - Ios
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)];
Solution 17 - Ios
In Swift 3.0
self.tableViewFeeds.setContentOffset(CGPoint(x: 0, y: CGFLOAT_MAX), animated: true)
Solution 18 - Ios
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.
Solution 19 - Ios
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 );
}
}
Solution 20 - Ios
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];
}
Solution 21 - Ios
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
}
}
}
}
Solution 22 - Ios
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)
}
}
Solution 23 - Ios
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.
Solution 24 - Ios
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
Solution 25 - Ios
[self.tableViewInfo scrollRectToVisible:CGRectMake(0, self.tableViewInfo.contentSize.height-self.tableViewInfo.height, self.tableViewInfo.width, self.tableViewInfo.height) animated:YES];
Solution 26 - Ios
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];
}
}
Solution 27 - Ios
No need for any scrolling you can just do it by using this code:
[YOURTABLEVIEWNAME setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
Solution 28 - Ios
If you are setting up frame for tableview programmatically, make sure you are setting frame correctly.
Solution 29 - Ios
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)
}
}
Solution 30 - Ios
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)
}
}
Solution 31 - Ios
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)
Solution 32 - Ios
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)
}
Solution 33 - Ios
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)
}
Solution 34 - Ios
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()
Solution 35 - Ios
In Swift, you just need
self.tableView.scrollToNearestSelectedRowAtScrollPosition(UITableViewScrollPosition.Bottom, animated: true)
to make it automatically scroll to the buttom