How to detect the end of loading of UITableView

IosIphoneUitableview

Ios Problem Overview


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.

Ios Solutions


Solution 1 - Ios

Improve to @RichX answer: lastRow can be both [tableView numberOfRowsInSection: 0] - 1 or ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. So the code will be:

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

UPDATE: Well, @htafoya's comment is right. If you want this code to detect end of loading all data from source, it wouldn't, but that's not the original question. This code is for detecting when all cells that are meant to be visible are displayed. willDisplayCell: used here for smoother UI (single cell usually displays fast after willDisplay: call). You could also try it with tableView:didEndDisplayingCell:.

Solution 2 - Ios

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

Solution 3 - Ios

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

Solution 4 - Ios

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.

Solution 5 - Ios

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

Solution 6 - Ios

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

Solution 7 - Ios

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.

Solution 8 - Ios

The best approach that I know is Eric's answer at: https://stackoverflow.com/questions/1483581/get-notified-when-uitableview-has-finished-asking-for-data/3060232#3060232

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

[tableView beginUpdates];
[tableView endUpdates];

Solution 9 - Ios

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.

Solution 10 - Ios

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

Solution 11 - Ios

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

Solution 12 - Ios

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

Solution 13 - Ios

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

Solution 14 - Ios

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

Solution 15 - Ios

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

Solution 16 - Ios

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

Solution 17 - Ios

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

Solution 18 - Ios

UITableView + Paging enable AND calling scrollToRow(..) to start on that page.

Best ugly workaround so far :/

override func viewDidLoad() {
    super.viewDidLoad()
    
    <#UITableView#>.reloadData()
    <#IUTableView#>.alpha = .zero
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
        self?.<#IUTableView#>.scrollToRow(at: <#IndexPath#>, at: .none, animated: true)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
             self?.<#IUTableView#>.alpha = 1
        }
    }
}

Solution 19 - Ios

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

Solution 20 - Ios

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.

Solution 21 - Ios

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

Solution 22 - Ios

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionemenegroView Question on Stackoverflow
Solution 1 - IosfolexView Answer on Stackoverflow
Solution 2 - IosDaniele CegliaView Answer on Stackoverflow
Solution 3 - IosRichXView Answer on Stackoverflow
Solution 4 - IosKyle CleggView Answer on Stackoverflow
Solution 5 - IosmalhalView Answer on Stackoverflow
Solution 6 - IosfatihyildizhanView Answer on Stackoverflow
Solution 7 - IosTravis M.View Answer on Stackoverflow
Solution 8 - IosRené RetzView Answer on Stackoverflow
Solution 9 - IosGaétanZView Answer on Stackoverflow
Solution 10 - IosOndrej KvasnovskyView Answer on Stackoverflow
Solution 11 - IosSuhas AithalView Answer on Stackoverflow
Solution 12 - IosAbdulMomen عبدالمؤمنView Answer on Stackoverflow
Solution 13 - Iosjpage4500View Answer on Stackoverflow
Solution 14 - IosumakantaView Answer on Stackoverflow
Solution 15 - IosCodetardView Answer on Stackoverflow
Solution 16 - IosDaniel McLeanView Answer on Stackoverflow
Solution 17 - IosKowbojView Answer on Stackoverflow
Solution 18 - IosalegelosView Answer on Stackoverflow
Solution 19 - IosDerekHView Answer on Stackoverflow
Solution 20 - Iosarango_86View Answer on Stackoverflow
Solution 21 - IosAndrew RaphaelView Answer on Stackoverflow
Solution 22 - Iospkc456View Answer on Stackoverflow