17

I use the idea of custom TableView pagination using willDisplayCell (or cellForRowAt) and updates from the server.

And the problem is that willDisplay called even for cells that are not on the screen.

How can I handle this behavior and change the flow to update cells only when user get scrolled to the last cell?

private func isLoadingIndexPath(_ indexPath: IndexPath) -> Bool {
    return indexPath.row == self.orders.count - 1
}

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    // fetch new data if user scroll to the last cell
    guard isLoadingIndexPath(indexPath) else { return }
    if self.totalItems > orders.count {
        fetchNextPage()
    }
}

private func fetchNextPage() {
    self.currentPage += 1
    self.delegate?.didReachLastCell(page: currentPage)
}

didReachLastCell calls the addOrders:

func addOrders(_ orders: [OrderView]) {
    self.orders.append(contentsOf: orders)
    reloadOrders()
}

fileprivate func reloadOrders() {
    self.tableView.reloadData()
}

Note: The same problem is reproducible for cellForRowAt method too.

atereshkov
  • 4,311
  • 1
  • 38
  • 49

8 Answers8

21

I had this problem when using UITableView.automaticDimension for cell height. I solved this problem using a higher value for estimatedRowHeight property.

Something like this:

tableView.estimatedRowHeight = 1000

Now willDisplay will be called only for rows which are visible.

Mukesh
  • 2,792
  • 15
  • 32
14

As @iWheeBuy said, willDisplayCell does not say that cell is on screen, so you can do it by checking in tableView.visibleCells.

However, even if this function is called when you scroll and it is going to be on screen tableView.visibleCells.contains(cell) will be false.

One approach that works for me is to make a delay, so full code is: (SWIFT 4)

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            if tableView.visibleCells.contains(cell) {
                // load more data
            }
        }
    }
Azzaro Mujic
  • 141
  • 1
  • 4
  • I'd be careful with this method if you are trying to use logic that needs to be called on all cells that appear on the screen. The cells will only be drawn once which means that your logic will *never* be called if the cell is drawn before it is visible. – btomtom5 Jul 25 '19 at 02:36
3

From documentation about tableView(_:willDisplay:forRowAt:):

A table view sends this message to its delegate just before it uses cell to draw a row, thereby permitting the delegate to customize the cell object before it is displayed. This method gives the delegate a chance to override state-based properties set earlier by the table view, such as selection and background color. After the delegate returns, the table view sets only the alpha and frame properties, and then only when animating rows as they slide in or out.

This description doesn't mention that the method will be called only for cells which are on the screen.

You can put your pagination logic to some other method / methods. For example:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    //
}

And check the state of the table using, for example, scrollView.contentOffset, scrollView.contentSize, tableView.indexPathsForVisibleRows and etc...

iWheelBuy
  • 5,470
  • 2
  • 37
  • 71
  • 3
    Be careful, `scrollViewDidScroll` is called very often, don't put heavy logic there. – iWheelBuy Jan 07 '18 at 11:40
  • Yea, I know this. Thx. But very strange, why the solution with cellForRow/willDisplayCell works for people from this issue? https://stackoverflow.com/a/35475282/5969121 – atereshkov Jan 07 '18 at 11:42
  • @atereshkov I have plenty of examples when the common solution doesn't work in some specific situation. I use `tableView(_:willDisplay:forRowAt:)` myself. But it is okay for my logic if this method is called for some offscreen indexPaths. Just search for a solution that suits you the most – iWheelBuy Jan 07 '18 at 12:03
  • @atereshkov What I found is that if you keep the table row height as Automatic Dimension, then this willDisplay is called even for the 20th cell which is way below the visible range. But if you keep the table row height as some constant one (lets say 50), then your pagination logic will work and willDisplayCell will only be called when the cell is about to display from the bottom. – Rajan Maheshwari May 24 '18 at 06:01
  • @iWheelBuy The description is fine but its weird that if you are making a table with 20 cells and at a time only 3 or 4 cells are visible but still for the first time you are getting the call for 20th cell as well in your willDisplayCell. In my above comment I described the automatic dimension matter. willDisplayCell is just like a delegate which tells a cell is going to be displayed. But I personally experienced a 20th cell display call even when it is far far below from visible range by keeping row height as Automatic. – Rajan Maheshwari May 24 '18 at 06:04
2

tableView(_:willDisplay:forRowAt:): This method when called, returns all the cells for the first time no matter whether all of them are visible or not. Therefore if you want to implement your pagination logic in this method then you have to check for visible cells first. i.e.

    if let visibleRows = tableView.indexPathsForVisibleRows , indexPath == visibleRows.last {
        guard isLoadingIndexPath(indexPath) else { return }
        if self.totalItems > orders.count {
            fetchNextPage()
        }
    }
      
  • 1
    This is not a true! This is not needed as problem can be with estimated row height. All cells should not be visible for the first time with the correct implementation. – Ondřej Korol Feb 09 '21 at 14:18
1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
   if tableView.visibleCells.contains(cell) {
      if (indexPath.row == self.arrProduct.count - 1){
                    if self.arrProduct.count > 0 {
                        if(self.arrProduct.count % self.RECORD_SET_LIMIT == 0 && self.isLoadMore) {
                            self.currentPage = self.currentPage + 1
                            print(self.currentPage)
                            self.perform(#selector(self.serviceGetProductByStatus), with: nil, afterDelay: 0.1)
                        }
                    }
                }
            }
        }
Raj Mohan
  • 957
  • 1
  • 6
  • 3
0

Implement these 3 scrollViewDelegate methods and using your fixed height cell num with current scrollOffset to know whether it's the last cell or not

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {

}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

}
//Pagination
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {

 }
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
0

In fact, there is the property prefetchingEnabled on UICollectionView that could suits your needs.

  • prefetchingEnabled

Summary

Denotes whether cell and data prefetching are enabled. Declaration

@property(nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled;

Discussion

When YES, the collection view requests cells in advance of when they will be displayed, spreading the rendering over multiple layout passes. When NO, the cells are requested as they are needed for display, often with multiple cells being requested in the same render loop. Setting this property to NO also disables data prefetching. The default value of this property is YES. Note When prefetching is enabled the collectionView:cellForItemAtIndexPath: method on the collection view delegate is called in advance of when the cell is required. To avoid inconsistencies in the visual appearance, use the collectionView:willDisplayCell:forItemAtIndexPath: delegate method to update the cell to reflect visual state such as selection.

Source : Apple Documentation

Neimsz
  • 1,554
  • 18
  • 22
0

In you your interface builder select the UITableView. Open size inspector and in the "Table View" section uncheck the checkbox next to "Estimate" row.

Kairat
  • 698
  • 1
  • 11
  • 14