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 vi
[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()
}
}
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)
}
}
}
}
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)
}
}
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()
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.
This works in Swift 3.0
let pointsFromTop = CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude)
tableView.setContentOffset(pointsFromTop, animated: true)