How to detect the end of loading of UITableView
Asked Answered
U

22

158

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.

Unashamed answered 12/11, 2010 at 10:14 Comment(3)
try a combination of datasource count and 'indexPathsForVisibleRows'Interesting
Re-opened based on further info in flag: "It's not duplicate. It asks about loading of visible cells, not about finish of data asking. See update to accepted answer"Vanillin
This solution works fine for me. You check it #1484081Delsiedelsman
W
231

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

Warnerwarning answered 26/7, 2012 at 15:8 Comment(7)
Much better for knowing when all the cells load that are visible.Stott
However this will be called whenever the user scrolls to view more cells.Cracksman
The problem with this is that it doesn't account for a footer view. Might not be an issue for most, but if it is you can apply the same idea but in the viewForFooterInSection method.Whorled
@KyleClegg patric.schenke mentioned one of reasons in comment to your answer. Also, what if footer isn't visible at the end of cell's loading?Warnerwarning
tableView:didEndDisplayingCell: is actually called when removing a cell from the view, not when its rendering in the view is complete so that won't work. Not a good method name. Gotta read the docs.Steadman
Thank you, I use this to detect finish scrolling too which mean all objects has been created.Untidy
This works for me except for when there is no data. The willDisplay doesn't fire. I need to know when the table is done loading even if there is no data.Ludwigshafen
B
48

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...
        }
    }
}
Befall answered 16/6, 2017 at 14:28 Comment(3)
this method gets called when we first time load data. In my case there are only 4 visible cells. I loaded 8 rows but still this function was getting called. What I want is to load more data when user scroll down to last rowKeratoid
Hi Muhammad Nayab, maybe you could replace "if indexPath == lastVisibleIndexPath" with "if indexPath.row == yourDataSource.count"Befall
@DanieleCeglia Thanks for point out this. if indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 this going to work for me. Cheers!!!Woodnote
Y
29

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];
    }
}
Yila answered 24/5, 2011 at 14:3 Comment(3)
how to detect the last row ? thats the problem for me .. can you explain how you get that lastrow into that if condition.Alcala
Last row is the [tableView numberOfRowsInSection: 0] - 1. You must replace 0 by needed value. But that's not the problem. Problem is that UITableView loads only visible. However ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row solves the problem.Warnerwarning
If we are looking for absolute completion wouldn't it be best to use - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath?Lunula
W
11

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.

Whorled answered 1/5, 2013 at 19:7 Comment(7)
Returning nil from tableView:viewForFooterInSection: messed with my layout in iOS 7 using Auto Layout.Chervonets
Interesting. What if you set it to a frame with height and width 0? return CGRectMake(0,0,0,0);Whorled
I tried that and even set self.tableView.sectionFooterHeight = 0. Either way, it seems to insert a section footer view with a height of about 10. I bet I could fix this by adding a 0-height constraint to the view I return. Anyway, I'm good because I actually wanted to figure out how to start UITableView on the last cell but saw this first.Chervonets
@MattDiPasquale if you implement viewForFooterInSection, you also have to implement heightForFooterInSection. It has to return 0 for sections with a nil footer. This is also in the official docs by now.Apperceive
viewForFooterInSection doesn't get called if you set the heightForFooterInSection to 0Torrez
i am having one section only for my tableview, so in viewForFooterInSection method that condition is executing everytime, but i want to execute that condition only when i reach my last row, is there any possibility to acheive that ?Iodize
@R.Mohan with only one section the footer would only show if the last row as been reached. Are you sure the last row isn't onscreen/loaded?Whorled
P
11

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)
        }
    }
}
Popinjay answered 13/12, 2014 at 0:37 Comment(6)
Much better solution than the others. It doesn't require a tableView reload. It's very simple and the viewDidLoadRows: doesn't get called every time the last cell is loaded.Oology
this is the best solution so far, since the other solutions doesnt take the multiple sections into accountLanchow
I'm sorry for the snarky comment, but how exactly is this 'magic'? Calling out to another method from delegate. I don't get it.Bremble
@LukasPetr take a look at the cancel and the afterDelay. Those enable the method call to be cancelled for every section except the last, which is when the table has completely finished loading.Popinjay
@Popinjay oh, okay. It is a bit more clever than I thought at first glance.Bremble
tableViewDidLoadRows method is calling everytime when the numberOfRowsInSection gets called, i am having only one section, i think it is happening due to that, am i right ?Iodize
S
11

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()
    }
}
Supportable answered 24/12, 2015 at 22:50 Comment(0)
I
8

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.

Inconclusive answered 21/7, 2016 at 20:54 Comment(0)
R
6

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];
Rockfish answered 13/1, 2011 at 13:46 Comment(1)
For waht it's worth, I've been using this very simple method on all my projects for months and it works perfectly.Lurid
C
6

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.

Christner answered 25/1, 2019 at 13:54 Comment(0)
M
1

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
    }

    // ....
}
Machine answered 5/10, 2016 at 17:10 Comment(0)
C
1

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

Cosme answered 27/10, 2016 at 7:21 Comment(0)
R
1

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()
    }
}
Rayburn answered 16/1, 2017 at 9:9 Comment(0)
B
0

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: https://mcmap.net/q/23859/-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..

Brief answered 23/7, 2014 at 20:24 Comment(1)
Welcome to SO @jpage4500. I have edited your answer to remove the stuff about low rep points and the question bump. Expanding on another user's answer is a perfectly valid answer on its own, so you can reduce the clutter by leaving that stuff out. It's good that you gave Andrew credit though, so that stayed.Hand
M
0

@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

 }
}
Mayfly answered 31/8, 2014 at 14:13 Comment(0)
M
0

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")
        }
}
Mesothelium answered 7/10, 2015 at 10:8 Comment(0)
D
0

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

        }
    }
}
Diversify answered 3/6, 2017 at 3:8 Comment(0)
R
0

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

Romelda answered 22/8, 2018 at 10:23 Comment(0)
M
0

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
        }
    }
}
Mucky answered 19/4, 2022 at 0:28 Comment(0)
D
-1

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

Duhl answered 12/11, 2010 at 16:44 Comment(0)
A
-1

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.

Aranyaka answered 20/1, 2016 at 6:48 Comment(0)
S
-3

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;
}
Steadman answered 10/3, 2014 at 18:10 Comment(0)
D
-3

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
                            })
Dual answered 22/9, 2015 at 10:13 Comment(3)
This method is only available on UICollectionView as far as I understand.Infix
Yes, you are right. UITableView's beginUpdates is equal to UICollectionView's performBatchUpdates:completion. https://mcmap.net/q/23863/-does-the-uitableview-quot-beginupdates-quot-and-uicollectionview-quot-performbatchupdates-quot-have-the-same-behaviorDual
So give a proper & valid answer to the question .Trisyllable

© 2022 - 2024 — McMap. All rights reserved.