user3126427 user3126427 - 1 month ago 29
iOS Question

How to insert rows to the top of UITableView without scrolling the table?

How to insert rows to the top of UITableView without scrolling the table? The same like when when you drag to reload new tweets on twitter or messages in whatsApp

I tried doing another section of begin/endUpdate asking it to scroll to the 3rd index, but it didn't work. It always scrolls to the top of the tableView. I want it to stay at the same position as it was before adding the new rows.

self.tableView.beginUpdates()
let indexPath = IndexPath(row: 3, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
self.tableView.endUpdates()


P.S. this is a sample project in my real project the cells varies in size based on the message size.

This is the link to the project:
https://github.com/akawther/TableView

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

@IBOutlet weak var tableView: UITableView!
var loadMore: UIRefreshControl!

var labels = ["A","B","C","D","E","F","G","H","I","J","K"]

override func viewDidLoad() {
super.viewDidLoad()

self.loadMore = UIRefreshControl()
self.loadMore.attributedTitle = NSAttributedString(string: "Pull to reload more")
self.loadMore.addTarget(self, action: #selector(ViewController.loadMoreChats), for: .valueChanged)
self.tableView.addSubview(self.loadMore)

// Do any additional setup after loading the view, typically from a nib.
}

func loadMoreChats() {
DispatchQueue.main.async(execute: {() -> Void in
self.tableView.beginUpdates()
self.labels.insert("3", at: 0)
self.labels.insert("2", at: 0)
self.labels.insert("1", at: 0)
let indexPaths = [
IndexPath(row: 0, section: 0),
IndexPath(row: 1, section: 0),
IndexPath(row: 2, section: 0),
]
self.tableView.insertRows(at: indexPaths, with: .top)
self.tableView.endUpdates()
self.loadMore.endRefreshing()

self.tableView.beginUpdates()
let indexPath = IndexPath(row: 3, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
self.tableView.endUpdates()
})
}


override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return labels.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell") as! customCellTableViewCell

cell.label.text = labels[indexPath.row]
return cell
}

}


UPDATE #1:
I have just tried it also by surrounding my second being/end update with CATransaction, but still the top displayed is 1 instead of A like I want:

CATransaction.begin()
self.tableView.beginUpdates()
CATransaction.setCompletionBlock { () -> Void in
let indexPath = IndexPath(row: 3, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
self.tableView.endUpdates()
CATransaction.commit()

Answer

This code worked fine for me when loadMoreChats was a target of a button:

    func loadMoreChats() {
        self.loadMore.endRefreshing()

        self.labels.insert("3", at: 0)
        self.labels.insert("2", at: 0)
        self.labels.insert("1", at: 0)

        self.tableView.reloadData()
        let indexPath = IndexPath(row: 3, section: 0)
        self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
    }

Using a refresh control cases the scrollToRow to be overridden by the pan gesture that triggered it.

Update

Note that the apps that you reference don't use a refresh controller in this way, but if you insist on using one I supposed you could disable the pan gesture briefly while you programmatically scroll the table view. Something like:

func loadMoreChats() {
    self.loadMore.endRefreshing()

    self.labels.insert("3", at: 0)
    self.labels.insert("2", at: 0)
    self.labels.insert("1", at: 0)

    self.tableView.reloadData()
    let indexPath = IndexPath(row: 3, section: 0)
    self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)

    self.tableView.panGestureRecognizer.isEnabled = false
    let when = DispatchTime.now() + 0.05
    DispatchQueue.main.asyncAfter(deadline: when)
    {
        self.tableView.panGestureRecognizer.isEnabled = true
    }
}

I find this to be rather hacky and would recommend against it.

Comments