Reputation: 3809
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
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
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
Reputation: 163248
I believe that calling
tableView.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)
will do what you want.
Upvotes: 154
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
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
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
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
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
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
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
Reputation: 25294
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)
}
}
tableView.scrollToBottom(animated: true)
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
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
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
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
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
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
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
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
Reputation: 1413
In Swift 3.0
self.tableViewFeeds.setContentOffset(CGPoint(x: 0, y: CGFLOAT_MAX), animated: true)
Upvotes: 3
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
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
Reputation: 1996
In Swift, you just need
self.tableView.scrollToNearestSelectedRowAtScrollPosition(UITableViewScrollPosition.Bottom, animated: true)
to make it automatically scroll to the buttom
Upvotes: -2
Reputation: 1263
If you are setting up frame for tableview programmatically, make sure you are setting frame correctly.
Upvotes: 1
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
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
Reputation: 666
No need for any scrolling you can just do it by using this code:
[YOURTABLEVIEWNAME setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
Upvotes: 1
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
Reputation: 21
[self.tableViewInfo scrollRectToVisible:CGRectMake(0, self.tableViewInfo.contentSize.height-self.tableViewInfo.height, self.tableViewInfo.width, self.tableViewInfo.height) animated:YES];
Upvotes: 1
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
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