Reputation: 791
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)
Upvotes: 78
Views: 92444
Reputation: 5039
I would use more generic approach to this:
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)
}
}
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)
}
}
Upvotes: 58
Reputation: 872
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)
}
}
Upvotes: 3
Reputation: 4064
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)
}
}
}
}
Upvotes: 2
Reputation: 520
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)
}
}
}
Upvotes: 0
Reputation: 240
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)
}
Upvotes: 5
Reputation: 275
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)
}
}
}
}
Upvotes: 1
Reputation: 171
You can use this one also:-
tableView.scrollRectToVisible(CGRect(x: 0, y: tableView.contentSize.height, width: 1, height: 1), animated: true)
Upvotes: 2
Reputation: 989
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)
Upvotes: 9
Reputation: 1284
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)
}
}
Upvotes: 7
Reputation: 224
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.
Upvotes: 16
Reputation: 2192
I tried Umair's approach, however in UITableView
s, 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)
}
}
Upvotes: 35
Reputation: 1413
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()
Upvotes: 121
Reputation: 336
[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()
}
}
Upvotes: 0
Reputation: 1700
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)
Upvotes: 0
Reputation: 143
Works in Swift 3+ :
self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentSize.height - UIScreen.main.bounds.height), animated: true)
Upvotes: -1
Reputation: 157
This works in Swift 3.0
let pointsFromTop = CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude)
tableView.setContentOffset(pointsFromTop, animated: true)
Upvotes: 1
Reputation: 16543
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.
Upvotes: 9