简体   繁体   中英

Adding cells to top of UITableView without interrupting user scrolling

I'm building a simple chat UI into my app but I'm having trouble with scrolling when prepending cells.

As the user scrolls up near the top I trigger a background load to get additional chat messages. When these arrive I prepend them (add them to the top of the table view).

The problem I'm seeing is that when the new cells come in, the table view scrolls all the way to the top.

What I'd like is that the user's position and scrolling direction/velocity doesn't get interrupted at all when the new cells are added.

I've read countless posts about this and I've tried all the proposed solutions (contentOffset reset, estimatedRowHeight caching, reloadData, insertRow, etc) but none seem to quite work.

Most solutions out there are for situations where cells are added to the bottom of the table view. This works fine because the contentOffset isn't affected when you reloadData . But when prepending cells to the top there's no way to know the new total height of all new cells that got added.

I should mention that I'm using self-sizing cells with dynamic heights. The cells contain dynamic text that could be multiple lines long.

In order to eliminate any possible quirks with the rest of my app I've created a simple stripped down app to illustrate the problem.

You can check it out on github and try it out yourselves: github.com/nebs/pagination-scroll-test .

The app simply simulates an infinite list of fake messages. When you scroll all the way to the top it loads and prepends more fake messages (with a 2 second simulated delay). All the code is in ViewController.swift

You can ignore most of the boilerplate code. The main part of interest is in this line at the reloadTableView() function .

The "best" (so far) solution I have is Attempt #4 (see code). Here, I simply remember the top-most rendered message, then scroll to that message again. It kind of works, but if the user is mid-scroll it will jitter a bit because it doesn't scroll exactly to where the user was.

The other 3 attempts in my code don't work (the table view jumps to the top after the new cells are added).

I'm curious to hear what solutions others have found for this. It seems like a trivial problem on paper but I've been at this for many days with no clear solution in sight.

What's the current best practice for solving this? Specifically for tableviews where (dynamic, variable-height, self-sizing) cells are prepended at the top while the user is scrolling.

I thank you all in advance for any tips you can provide!

I ended up going with a solution that works quite well. I'll post it here in case it helps someone else in the future. Still open to other answers.

Basically I find the message that is currently visible at the top of the view frame and I find its offset to the top. Then when the data loads I find that message again, find its new index path and scroll to there along with the offset. This has the effect of placing the user exactly to where they were (visually-speaking, of course when the cells are loaded the user is technically further down in the list).

I posted a commit to the linked github project with this new solution (it's under "Attempt #5", just uncomment that bit and comment out Attempt #1" to test it out.)

    var currentMessage: Message? = nil
var offsetFromTopOfMessage: CGFloat = 0
if let topIndexPath = self.tableView.indexPathsForVisibleRows?.first {
    var topMessageRect = self.tableView.rectForRow(at: topIndexPath)
    topMessageRect = topMessageRect.offsetBy(dx: -tableView.contentOffset.x, dy: -tableView.contentOffset.y)
    offsetFromTopOfMessage = topMessageRect.minY
    currentMessage = self.messages[topIndexPath.row]
}
self.generateMoreMessages()
self.tableView.reloadData()
if let targetMessage = currentMessage,
    let targetIndex = (self.messages.index{$0.title == targetMessage.title}) {
    let targetIndexPath = IndexPath(row: targetIndex, section: 0)
    self.tableView.scrollToRow(at: targetIndexPath, at: .top, animated: false)
    self.tableView.contentOffset.y -= offsetFromTopOfMessage
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM