[ios] How to detect the end of loading of UITableView

I want to change the offset of the table when the load is finished and that offset depends on the number of cells loaded on the table.

Is it anyway on the SDK to know when a uitableview loading has finished? I see nothing neither on delegate nor on data source protocols.

I can't use the count of the data sources because of the loading of the visible cells only.

This question is related to ios iphone uitableview

The answer is


Swift 2 solution:

// willDisplay function
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    let lastRowIndex = tableView.numberOfRowsInSection(0)
    if indexPath.row == lastRowIndex - 1 {
        fetchNewDataFromServer()
    }
}

// data fetcher function
func fetchNewDataFromServer() {
    if(!loading && !allDataFetched) {
        // call beginUpdates before multiple rows insert operation
        tableView.beginUpdates()
        // for loop
        // insertRowsAtIndexPaths
        tableView.endUpdates()
    }
}

Objective C

[self.tableView reloadData];
[self.tableView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// table-view finished reload
                              }];

Swift

self.tableView?.reloadData()
self.tableView?.performBatchUpdates({ () -> Void in

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })

Are you looking for total number of items that will be displayed in the table or total of items currently visible? Either way.. I believe that the 'viewDidLoad' method executes after all the datasource methods are called. However, this will only work on the first load of the data(if you are using a single alloc ViewController).


Here's another option that seems to work for me. In the viewForFooter delegate method check if it's the final section and add your code there. This approach came to mind after realizing that willDisplayCell doesn't account for footers if you have them.

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section 
{
  // Perform some final layout updates
  if (section == ([tableView numberOfSections] - 1)) {
    [self tableViewWillFinishLoading:tableView];
  }

  // Return nil, or whatever view you were going to return for the footer
  return nil;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
  // Return 0, or the height for your footer view
  return 0.0;
}

- (void)tableViewWillFinishLoading:(UITableView *)tableView
{
  NSLog(@"finished loading");
}

I find this approach works best if you are looking to find the end loading for the entire UITableView, and not simply the visible cells. Depending on your needs you may only want the visible cells, in which case folex's answer is a good route.


I know this is answered, I am just adding a recommendation.

As per the following documentation

https://www.objc.io/issues/2-concurrency/thread-safe-class-design/

Fixing timing issues with dispatch_async is a bad idea. I suggest we should handle this by adding FLAG or something.


Here is how you do it in Swift 3:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    if indexPath.row == 0 {
        // perform your logic here, for the first row in the table
    }

    // ....
}

I am copying Andrew's code and expanding it to account for the case where you just have 1 row in the table. It's working so far for me!

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// detect when all visible cells have been loaded and displayed
// NOTE: iOS7 workaround used - see: http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview?lq=1
NSArray *visibleRows = [tableView indexPathsForVisibleRows];
NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
BOOL isPreviousCallForPreviousCell = self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
BOOL isFinishedLoadingTableView = isLastCell && ([tableView numberOfRowsInSection:0] == 1 || isPreviousCallForPreviousCell);

self.previousDisplayedIndexPath = indexPath;

if (isFinishedLoadingTableView) {
    [self hideSpinner];
}
}

NOTE: I'm just using 1 section from Andrew's code, so keep that in mind..


If you have multiple sections, here's how to get the last row in the last section (Swift 3):

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let visibleRows = tableView.indexPathsForVisibleRows, let lastRow = visibleRows.last?.row, let lastSection = visibleRows.map({$0.section}).last {
        if indexPath.row == lastRow && indexPath.section == lastSection {
            // Finished loading visible rows

        }
    }
}

@folex answer is right.

But it will fail if the tableView has more than one section displayed at a time.

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
   if([indexPath isEqual:((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject])]){
    //end of loading

 }
}

In iOS7.0x the solution is a bit different. Here is what I came up with.

    - (void)tableView:(UITableView *)tableView 
      willDisplayCell:(UITableViewCell *)cell 
    forRowAtIndexPath:(NSIndexPath *)indexPath
{
    BOOL isFinishedLoadingTableView = [self isFinishedLoadingTableView:tableView  
                                                             indexPath:indexPath];
    if (isFinishedLoadingTableView) {
        NSLog(@"end loading");
    }
}

- (BOOL)isFinishedLoadingTableView:(UITableView *)tableView 
                         indexPath:(NSIndexPath *)indexPath
{
    // The reason we cannot just look for the last row is because 
    // in iOS7.0x the last row is updated before
    // looping through all the visible rows in ascending order 
    // including the last row again. Strange but true.
    NSArray * visibleRows = [tableView indexPathsForVisibleRows];   // did verify sorted ascending via logging
    NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
    // For tableviews with multiple sections this will be more complicated.
    BOOL isPreviousCallForPreviousCell = 
             self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
    BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
    BOOL isFinishedLoadingTableView = isLastCell && isPreviousCallForPreviousCell;
    self.previousDisplayedIndexPath = indexPath;
    return isFinishedLoadingTableView;
}

To know when a table view finishes loading its content, we first need to have a basic understanding of how the views are put on screen.

In the life cycle of an app, there are 4 key moments :

  1. The app receives an event (touch, timer, block dispatched etc)
  2. The app handles the event (it modifies a constraint, starts an animation, changes background etc)
  3. The app computes the new view hierarchy
  4. The app renders the view hierarchy and displays it

The 2 and 3 times are totally separated. Why ? For performance reasons, we don't want to perform all the computations (done at 3) each time a modification is done.

So, I think you are facing a case like this :

tableView.reloadData()
tableView.visibleCells.count // wrong count oO

What’s wrong here?

A table view reloads its content lazily. Actually, if you call reloadData multiple times it won’t create performance issues. The table view only recomputes its content size based on its delegate implementation and waits the moment 3 to loads its cells. This time is called a layout pass.

Okay, how to get involved in the layout pass?

During the layout pass, the app computes all the frames of the view hierarchy. To get involved, you can override the dedicated methods layoutSubviews, updateLayoutConstraints etc in a UIView subclass and the equivalent methods in a view controller subclass.

That’s exactly what a table view does. It overrides layoutSubviews and based on your delegate implementation adds or removes cells. It calls cellForRow right before adding and laying out a new cell, willDisplay right after. If you called reloadData or just added the table view to the hierarchy, the tables view adds as many cells as necessary to fill its frame at this key moment.

Alright, but now, how to know when a tables view has finished reloading its content?

We can rephrase this question: how to know when a table view has finished laying out its subviews?

The easiest way is to get into the layout of the table view :

class MyTableView: UITableView {
    func layoutSubviews() {
        super.layoutSubviews()
        // the displayed cells are loaded
    }
}

Note that this method is called many times in the life cycle of the table view. Because of the scroll and the dequeue behavior of the table view, cells are modified, removed and added often. But it works, right after the super.layoutSubviews(), cells are loaded. This solution is equivalent to wait the willDisplay event of the last index path. This event is called during the execution of layoutSubviews of the table view when a cell is added.

Another way is to be notified when the app finishes a layout pass.

As described in the documentation, you can use an option of the UIView.animate(withDuration:completion):

tableView.reloadData()
UIView.animate(withDuration: 0) {
    // layout done
}

This solution works but the screen will refresh once between the time the layout is done and the time the block is executed. This is equivalent to the DispatchMain.async solution but specified.

Alternatively, I would prefer to force the layout of the table view

There is a dedicated method to force any view to compute immediately its subview frames layoutIfNeeded:

tableView.reloadData()
table.layoutIfNeeded()
// layout done

Be careful however, doing so will remove the lazy loading used by the system. Calling those methods repeatedly could create performance issues. Make sure that they won’t be called before the frame of the table view is computed, otherwise the table view will be loaded again and you won’t be notified.


I think there is no perfect solution. Subclassing classes could lead to trubles. A layout pass starts from the top and goes to the bottom so it’s not easy to get notified when all the layout is done. And layoutIfNeeded() could create performance issues etc.


Quite accidentally I bumped into this solution:

tableView.tableFooterView = UIView()
tableViewHeight.constant = tableView.contentSize.height

You need to set the footerView before getting the contentSize e.g. in viewDidLoad. Btw. setting the footeView lets you delete "unused" separators


Swift 3 & 4 & 5 version:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}

here is how I do it in Swift 3

let threshold: CGFloat = 76.0 // threshold from bottom of tableView

internal func scrollViewDidScroll(_ scrollView: UIScrollView) {

    let contentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    if  (!isLoadingMore) &&  (maximumOffset - contentOffset <= threshold) {
        self.loadVideosList()
    }
}

I always use this very simple solution:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

For the chosen answer version in Swift 3:

var isLoadingTableView = true

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if tableData.count > 0 && isLoadingTableView {
        if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows, let lastIndexPath = indexPathsForVisibleRows.last, lastIndexPath.row == indexPath.row {
            isLoadingTableView = false
            //do something after table is done loading
        }
    }
}

I needed the isLoadingTableView variable because I wanted to make sure the table is done loading before I make a default cell selection. If you don't include this then every time you scroll the table it will invoke your code again.


The best approach that I know is Eric's answer at: Get notified when UITableView has finished asking for data?

Update: To make it work I have to put these calls in -tableView:cellForRowAtIndexPath:

[tableView beginUpdates];
[tableView endUpdates];

Here is what I would do.

  1. In your base class (can be rootVC BaseVc etc),

    A. Write a Protocol to send the "DidFinishReloading" callback.

    @protocol ReloadComplition <NSObject>
    @required
    - (void)didEndReloading:(UITableView *)tableView;
    @end
    

    B. Write a generic method to reload the table view.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
    
  2. In the base class method implementation, call reloadData followed by delegateMethod with delay.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController{
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [tableView reloadData];
            if(aViewController && [aViewController respondsToSelector:@selector(didEndReloading:)]){
                [aViewController performSelector:@selector(didEndReloading:) withObject:tableView afterDelay:0];
            }
        }];
    }
    
  3. Confirm to the reload completion protocol in all the view controllers where you need the callback.

    -(void)didEndReloading:(UITableView *)tableView{
        //do your stuff.
    }
    

Reference: https://discussions.apple.com/thread/2598339?start=0&tstart=0


Using private API:

@objc func tableViewDidFinishReload(_ tableView: UITableView) {
    print(#function)
    cellsAreLoaded = true
}

Using public API:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // cancel the perform request if there is another section
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];

    // create a perform request to call the didLoadRows method on the next event loop.
    [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];

    return [self.myDataSource numberOfRowsInSection:section];
}

// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
    self.cellsAreLoaded = YES;
}

A possible better design is to add the visible cells to a set, then when you need to check if the table is loaded you can instead do a for loop around this set, e.g.

var visibleCells = Set<UITableViewCell>()

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.insert(cell)
}

override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.remove(cell)
}

// example property you want to show on a cell that you only want to update the cell after the table is loaded. cellForRow also calls configure too for the initial state.
var count = 5 {
    didSet {
        for cell in visibleCells {
            configureCell(cell)
        }
    }
}

In Swift you can do something like this. Following condition will be true every time you reach end of the tableView

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row+1 == postArray.count {
            println("came to last row")
        }
}

Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to iphone

Detect if the device is iPhone X Xcode 8 shows error that provisioning profile doesn't include signing certificate Access files in /var/mobile/Containers/Data/Application without jailbreaking iPhone Certificate has either expired or has been revoked Missing Compliance in Status when I add built for internal testing in Test Flight.How to solve? cordova run with ios error .. Error code 65 for command: xcodebuild with args: "Could not find Developer Disk Image" Reason: no suitable image found iPad Multitasking support requires these orientations How to insert new cell into UITableView in Swift

Examples related to uitableview

Adding a UISegmentedControl to UITableView make UITableViewCell selectable only while editing How to fix Error: this class is not key value coding-compliant for the key tableView.' UITableView example for Swift Swift - how to make custom header for UITableView? How to insert new cell into UITableView in Swift How to detect tableView cell touched or clicked in swift Dynamic Height Issue for UITableView Cells (Swift) UIButton action in table view cell How to get the indexpath.row when an element is activated?