atereshkov
atereshkov

Reputation: 4555

TableView willDisplayCell called for cells that are not on the screen (tableView pagination)

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.

Upvotes: 17

Views: 23022

Answers (8)

Pranjal Singh
Pranjal Singh

Reputation: 29

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()
        }
    }
      

Upvotes: 2

Kairat
Kairat

Reputation: 788

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

Upvotes: 0

Raj Mohan
Raj Mohan

Reputation: 1237

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)
                        }
                    }
                }
            }
        }

Upvotes: 1

Neimsz
Neimsz

Reputation: 1554

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

Upvotes: 0

Mukesh
Mukesh

Reputation: 2902

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.

Upvotes: 21

Azzaro Mujic
Azzaro Mujic

Reputation: 141

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
            }
        }
    }

Upvotes: 14

iWheelBuy
iWheelBuy

Reputation: 5679

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...

Upvotes: 3

Shehata Gamal
Shehata Gamal

Reputation: 100503

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) {

 }

Upvotes: 0

Related Questions