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.

Lin Du
  • 88,126
  • 95
  • 281
  • 483
emenegro
  • 6,901
  • 10
  • 45
  • 68
  • try a combination of datasource count and 'indexPathsForVisibleRows' – Swapnil Luktuke Nov 12 '10 at 11:52
  • 1
    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" – Kev Mar 16 '13 at 17:57
  • This solution works fine for me. You check it http://stackoverflow.com/questions/1483581/get-notified-when-uitableview-has-finished-asking-for-data#21581834 – Khaled Annajar Jun 22 '15 at 13:31

22 Answers22

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

folex
  • 5,122
  • 1
  • 28
  • 48
  • Much better for knowing when all the cells load that are visible. – Eric Sep 26 '12 at 21:44
  • 14
    However this will be called whenever the user scrolls to view more cells. – htafoya Mar 07 '13 at 18:47
  • 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. – Kyle Clegg Oct 11 '13 at 13:00
  • 1
    @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? – folex Mar 01 '14 at 17:24
  • 32
    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. – Andrew Raphael Mar 10 '14 at 18:02
  • Thank you, I use this to detect finish scrolling too which mean all objects has been created. – Dody Rachmat Wicaksono Oct 12 '16 at 01:11
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...
        }
    }
}
Daniele Ceglia
  • 849
  • 8
  • 12
  • 1
    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 row – Muhammad Nayab Dec 12 '18 at 07:09
  • Hi Muhammad Nayab, maybe you could replace "if indexPath == lastVisibleIndexPath" with "if indexPath.row == yourDataSource.count" – Daniele Ceglia Dec 18 '18 at 09:50
  • 1
    @DanieleCeglia Thanks for point out this. if indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 this going to work for me. Cheers!!! – Yogesh Patel Feb 05 '20 at 09:54
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];
    }
}
RichX
  • 494
  • 5
  • 13
  • how to detect the last row ? thats the problem for me .. can you explain how you get that lastrow into that if condition. – Sameera Chathuranga Jul 03 '12 at 12:48
  • 6
    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. – folex Jul 26 '12 at 15:05
  • 1
    If we are looking for absolute completion wouldn't it be best to use - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath? – morcutt Apr 18 '13 at 02:04
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.

Community
  • 1
  • 1
Kyle Clegg
  • 38,547
  • 26
  • 130
  • 141
  • Returning `nil` from `tableView:viewForFooterInSection:` messed with my layout in iOS 7 using Auto Layout. – ma11hew28 Oct 14 '13 at 19:09
  • Interesting. What if you set it to a frame with height and width 0? `return CGRectMake(0,0,0,0);` – Kyle Clegg Oct 14 '13 at 19:12
  • 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](http://stackoverflow.com/questions/2156614/how-to-start-uitableview-on-the-last-cell) but saw this first. – ma11hew28 Oct 15 '13 at 18:03
  • 1
    @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. – patric.schenke Dec 17 '13 at 08:45
  • viewForFooterInSection doesn't get called if you set the heightForFooterInSection to 0 – Oded Harth Apr 12 '15 at 13:00
  • 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 ? – R. Mohan Jul 28 '17 at 09:29
  • @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? – Kyle Clegg Jul 28 '17 at 16:38
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)
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
  • 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. – Matt Hudson Nov 10 '15 at 01:16
  • this is the best solution so far, since the other solutions doesnt take the multiple sections into account – justicepenny Jun 28 '16 at 08:52
  • 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. – Lukas Petr Jan 23 '17 at 05:07
  • @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. – malhal Jan 23 '17 at 16:01
  • @malhal oh, okay. It is a bit more clever than I thought at first glance. – Lukas Petr Jan 28 '17 at 11:48
  • 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 ? – R. Mohan Jul 28 '17 at 09:34
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()
    }
}
fatihyildizhan
  • 8,614
  • 7
  • 64
  • 88
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.

Elijah
  • 8,381
  • 2
  • 55
  • 49
Travis M.
  • 10,930
  • 1
  • 56
  • 72
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];
Community
  • 1
  • 1
René Retz
  • 81
  • 1
  • 6
  • For waht it's worth, I've been using this very simple method on all my projects for months and it works perfectly. – Eric MORAND Mar 22 '11 at 21:35
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.

GaétanZ
  • 4,870
  • 1
  • 23
  • 30
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
    }

    // ....
}
Ondrej Kvasnovsky
  • 4,592
  • 3
  • 30
  • 40
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

Pang
  • 9,564
  • 146
  • 81
  • 122
Suhas Aithal
  • 842
  • 8
  • 20
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()
    }
}
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: 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..

skrrgwasme
  • 9,358
  • 11
  • 54
  • 84
jpage4500
  • 951
  • 9
  • 11
  • 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. – skrrgwasme Jul 23 '14 at 20:49
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

 }
}
umakanta
  • 1,051
  • 19
  • 25
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")
        }
}
Codetard
  • 2,441
  • 28
  • 34
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

        }
    }
}
Daniel McLean
  • 451
  • 7
  • 10
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

Kowboj
  • 351
  • 2
  • 14
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
        }
    }
}
alegelos
  • 2,308
  • 17
  • 26
-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.

arango_86
  • 4,236
  • 4
  • 40
  • 46
-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).

DerekH
  • 860
  • 5
  • 11
-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;
}
Andrew Raphael
  • 507
  • 2
  • 7
  • 9
-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
                            })
pkc456
  • 8,350
  • 38
  • 53
  • 109