I have a table view over very large amount of data. For performance reasons, it can't be loaded all at once. More, sometimes random place of array should be loaded, so incremental pagination is not good option.
The current solution to these requirements is sliding window over data array. As user scrolls, I add cells from one end and remove from opposite end. I use scroll position (by looking at what cells are going onscreen) to determine whether it's time to load new data.
Usually, when you call tableView.deleteRows(at:with:)
and remove cells from the beginning of table, tableView adjusts its contentOffset
property so user still see same cells as before operation.
However, when tableView is decelerating after scrolling, its contentOffset
is not adjusted on updates, and this causes loading new pages over and over until deceleration is completed. Then, on first update after deceleration, contentOffset
is fixed by tableView and loading stops.
Same thing occurs when scrolling back and adding values at the beginning of table with tableView.insertRows(at:with:)
.
How can I make UITableView adjust its contentOffset properly?
OR are there other ways to overcome this bug -- keeping the ability to load arbitrary piece in the middle of data array and scroll from it?
I made a tiny project illustrating the bug:
https://github.com/wsb9/TableViewExample
From your sample project, I can understand is you are trying to implement infinite scroll through the window content concept so that you can always have fixed number of rows (index paths ,lets say 100) so that when window scrolls down/up - table view remove indexPaths from top/bottom accordingly. And even though you have more data source item you can always have tableView of indexPaths 100
Basically you are dealing with two problem here:
ContentOffset
Dynamic height
Let's assume we have height is fixed (44) and table is not inverted. To implement Window for infinite scrolling you have to do following:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height
let buffer: CGFloat = 3 * 44
let scrollPosition = scrollView.contentOffset.y
if (scrollPosition > bottom - buffer) {
dataSource.expose(dataSource.exposedRange.shift(by: 25))
self.tableView.contentOffset.y -= self.dataSource.deltaHeightToRemove
}
}
In this way total content size will be fixed every time
Hope it will help.
EDIT:- Here is the modified code which actually does infinite scroll at bottom of table view with dynamic cell height. This doesn't increase the rows count more than 100. but still loads data in sliding window. link
From your sample project, I can understand the following,
I checked your code and you have implemented your sliding-window over data-source model
very interestingly. The problem caused because of you had been trying to make tableview
efficiently by removing and readding cells.
Actually the Dequeuing a cell should be reusing a cell already in memory. Please take a look at the apple documentation,
For performance reasons, a table view’s data source should generally reuse UITableViewCell objects when it assigns cells to rows in its tableView(_:cellForRowAt:) method. A table view maintains a queue or list of UITableViewCell objects that the data source has marked for reuse. Call this method from your data source object when asked to provide a new cell for the table view. This method dequeues an existing cell if one is available or creates a new one using the class or nib file you previously registered. If no cell is available for reuse and you did not register a class or nib file, this method returns nil.
The good news is your sliding-window over data-source model
is working perfectly once I removed your row delete and readd mechanism. Here is your working code,
https://drive.google.com/file/d/0B2y_JJbzjRA6dDR3QzRMUzExSGs/view?usp=sharing
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With