Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableView does not handle contentOffset correctly when scrolling/paging

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

like image 468
TIMEX Avatar asked Sep 18 '17 18:09

TIMEX


Video Answer


2 Answers

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:

  1. ContentOffset

  2. 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
        }
    }
  • Decide how much height buffer you need to keep when scroll goes down. This height buffer is the height after which you decides to insert some more item (25) into the datasource.
  • At this point you now have to remove items from the top
  • As you remove item from top you are basically telling scrollView to reduce it's content-offset by same height.
  • 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

like image 96
manismku Avatar answered Oct 11 '22 02:10

manismku


From your sample project, I can understand the following,

  • One thing is you want to increase performance of your table view by only loading few number of cells at a time
  • Your second concern is sometimes you want to load table view with data that randomly placed in data source array

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

like image 2
arunjos007 Avatar answered Oct 11 '22 00:10

arunjos007