How to tell when UITableVIew has completed ReloadData?

I am trying to scroll to the bottom of a UITableView after it is done performing [self.tableView reloadData]

I originally had

  • Offline Game Center iOS Achievements
  • OCMock and block testing, executing
  • UILocalNotification firedate from picker
  • Set UILabel line spacing
  • send a notification from javascript in UIWebView to ObjectiveC
  • insertObject: atIndex: - index 3 beyond bounds for empty array
  •  [self.tableView reloadData]
     NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
    
    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
    

    But then I read that reloadData is asynchronous, so the scrolling doesn’t happen since the self.tableView, [self.tableView numberOfSections] and [self.tableView numberOfRowsinSection are all 0.

    Thanks!

    What’s weird is that I am using:

    [self.tableView reloadData];
    NSLog(@"Number of Sections %d", [self.tableView numberOfSections]);
    NSLog(@"Number of Rows %d", [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);
    

    In the console it returns Sections = 1, Row = -1;

    When I do the exact same NSLogs in cellForRowAtIndexPath I get Sections = 1 and Row = 8; (8 is right)

    13 Solutions Collect From Internet About “How to tell when UITableVIew has completed ReloadData?”

    The reload happens during the next layout pass, which normally happens when you return control to the run loop (after, say, your button action or whatever returns).

    So one way to run something after the table view reloads is simply to force the table view to perform layout immediately:

    [self.tableView reloadData];
    [self.tableView layoutIfNeeded];
     NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
    

    Another way is to schedule your after-layout code to run later using dispatch_async:

    [self.tableView reloadData];
    
    dispatch_async(dispatch_get_main_queue(), ^{
         NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection:([self.tableView numberOfSections]-1)];
    
        [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
    });
    

    UPDATE

    Upon further investigation, I find that the table view sends tableView:numberOfRowsInSection: to its data source before returning from reloadData. If the delegate implements tableView:heightForRowAtIndexPath:, the table view also sends that (for each row) before returning from reloadData.

    However, the table view does not send tableView:cellForRowAtIndexPath: until the layout phase, which happens by default when you return control to the run loop.

    And I also find that in a tiny test program, the code in your question properly scrolls to the bottom of the table view, without me doing anything special (like sending layoutIfNeeded or using dispatch_async).

    Swift:

    extension UITableView {
        func reloadData(completion: ()->()) {
            UIView.animateWithDuration(0, animations: { self.reloadData() })
                { _ in completion() }
        }
    }
    
    ...somewhere later...
    
    tableView.reloadData {
        println("done")
    }
    

    Objective-C:

    [UIView animateWithDuration:0 animations:^{
        [myTableView reloadData];
    } completion:^(BOOL finished) {
        //Do something after that...
    }];
    

    The dispatch_async(dispatch_get_main_queue()) method above is not guaranteed to work. I’m seeing non-deterministic behavior with it, in which sometimes the system has completed the layoutSubviews and the cell rendering before the completion block, and sometimes after.

    Here’s a solution that works 100% for me, on iOS 10. It requires the ability to instantiate the UITableView or UICollectionView as a custom subclass. Here’s the UICollectionView solution, but it’s exactly the same for UITableView:

    CustomCollectionView.h:

    #import <UIKit/UIKit.h>
    
    @interface CustomCollectionView: UICollectionView
    
    - (void)reloadDataWithCompletion:(void (^)(void))completionBlock;
    
    @end
    

    CustomCollectionView.m:

    #import "CustomCollectionView.h"
    
    @interface CustomCollectionView ()
    
    @property (nonatomic, copy) void (^reloadDataCompletionBlock)(void);
    
    @end
    
    @implementation CustomCollectionView
    
    - (void)reloadDataWithCompletion:(void (^)(void))completionBlock
    {
        self.reloadDataCompletionBlock = completionBlock;
        [super reloadData];
    }
    
    - (void)layoutSubviews
    {
        [super layoutSubviews];
    
        if (self.reloadDataCompletionBlock) {
            self.reloadDataCompletionBlock();
            self.reloadDataCompletionBlock = nil;
        }
    }
    
    @end
    

    Example usage:

    [self.collectionView reloadDataWithCompletion:^{
        // reloadData is guaranteed to have completed
    }];
    

    I had the same issues as Tyler Sheaffer.

    I implemented his solution in Swift and it solved my problems.

    Swift 3.0:

    final class UITableViewWithReloadCompletion: UITableView {
      private var reloadDataCompletionBlock: (() -> Void)?
    
      override func layoutSubviews() {
        super.layoutSubviews()
    
        reloadDataCompletionBlock?()
        reloadDataCompletionBlock = nil
      }
    
    
      func reloadDataWithCompletion(completion: @escaping () -> Void) {
        reloadDataCompletionBlock = completion
        super.reloadData()
      }
    }
    

    Swift 2:

    class UITableViewWithReloadCompletion: UITableView {
    
      var reloadDataCompletionBlock: (() -> Void)?
    
      override func layoutSubviews() {
        super.layoutSubviews()
    
        self.reloadDataCompletionBlock?()
        self.reloadDataCompletionBlock = nil
      }
    
      func reloadDataWithCompletion(completion:() -> Void) {
          reloadDataCompletionBlock = completion
          super.reloadData()
      }
    }
    

    Example Usage:

    tableView.reloadDataWithCompletion() {
     // reloadData is guaranteed to have completed
    }
    

    As of Xcode 8.2.1, iOS 10, and swift 3,

    You can determine the end of tableView.reloadData() easily by using a CATransaction block:

    CATransaction.begin()
    CATransaction.setCompletionBlock({
        print("reload completed")
        //Your completion code here
    )}
    print("reloading")
    tableView.reloadData()
    CATransaction.commit()
    

    The above also works for determining the end of UICollectionView’s reloadData() and UIPickerView’s reloadAllComponents().

    It appears folks are still reading this question and the answers. B/c of that, I’m editing my answer to remove the word Synchronous which is really irrelevant to this.

    When [tableView reloadData] returns, the internal data structures behind the tableView have been updated. Therefore, when the method completes you can safely scroll to the bottom. I verified this in my own app. The widely accepted answer by @rob-mayoff, while also confusing in terminology, acknowledges the same in his last update.

    If your tableView isn’t scrolling to the bottom you may have an issue in other code you haven’t posted. Perhaps you are changing data after scrolling is complete and you’re not reloading and/or scrolling to the bottom then?

    Add some logging as follows to verify that the table data is correct after reloadData. I have the following code in a sample app and it works perfectly.

    // change the data source
    
    NSLog(@"Before reload / sections = %d, last row = %d",
          [self.tableView numberOfSections],
          [self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);
    
    [self.tableView reloadData];
    
    NSLog(@"After reload / sections = %d, last row = %d",
          [self.tableView numberOfSections],
          [self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);
    
    [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]-1
                                                              inSection:[self.tableView numberOfSections] - 1]
                          atScrollPosition:UITableViewScrollPositionBottom
                                  animated:YES];
    

    I use this trick, pretty sure I already posted it to a duplicate of this question:

    -(void)tableViewDidLoadRows:(UITableView *)tableView{
        // do something after loading, e.g. select a cell.
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        // trick to detect when table view has finished loading.
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];
        [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];
    
        // specific to your controller
        return self.objects.count;
    }
    

    Actually this one solved my problem:

    -(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@"section"]];
    if (visibleSections) {
        // hide the activityIndicator/Loader
    }}
    

    Try this way it will work

    [tblViewTerms performSelectorOnMainThread:@selector(dataLoadDoneWithLastTermIndex:) withObject:lastTermIndex waitUntilDone:YES];waitUntilDone:YES];
    
    @interface UITableView (TableViewCompletion)
    
    -(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex;
    
    @end
    
    @implementation UITableView(TableViewCompletion)
    
    -(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex
    {
        NSLog(@"dataLoadDone");
    
    
    NSIndexPath* indexPath = [NSIndexPath indexPathForRow: [lastTermIndex integerValue] inSection: 0];
    
    [self selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
    
    }
    @end
    

    I will execute when table is completely loaded

    Other Solution is you can subclass UITableView

    I ended up using a variation of Shawn’s solution:

    Create a custom UITableView class with a delegate:

    protocol CustomTableViewDelegate {
        func CustomTableViewDidLayoutSubviews()
    }
    
    class CustomTableView: UITableView {
    
        var customDelegate: CustomTableViewDelegate?
    
        override func layoutSubviews() {
            super.layoutSubviews()
            self.customDelegate?.CustomTableViewDidLayoutSubviews()
        }
    }
    

    Then in my code, I use

    class SomeClass: UIViewController, CustomTableViewDelegate {
    
        @IBOutlet weak var myTableView: CustomTableView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.myTableView.customDelegate = self
        }
    
        func CustomTableViewDidLayoutSubviews() {
            print("didlayoutsubviews")
            // DO other cool things here!!
        }
    }
    

    Also make sure you set your table view to CustomTableView in the interface builder:

    enter image description here

    And a UICollectionView version, based on kolaworld’s answer:

    https://stackoverflow.com/a/43162226/1452758

    Needs testing. Works so far on iOS 9.2, Xcode 9.2 beta 2, with scrolling a collectionView to an index, as a closure.

    extension UICollectionView
    {
        /// Calls reloadsData() on self, and ensures that the given closure is
        /// called after reloadData() has been completed.
        ///
        /// Discussion: reloadData() appears to be asynchronous. i.e. the
        /// reloading actually happens during the next layout pass. So, doing
        /// things like scrolling the collectionView immediately after a
        /// call to reloadData() can cause trouble.
        ///
        /// This method uses CATransaction to schedule the closure.
    
        func reloadDataThenPerform(_ closure: @escaping (() -> Void))
        {       
            CATransaction.begin()
                CATransaction.setCompletionBlock(closure)
                self.reloadData()
            CATransaction.commit()
        }
    }
    

    Usage:

    myCollectionView.reloadDataThenPerform {
        myCollectionView.scrollToItem(at: indexPath,
                at: .centeredVertically,
                animated: true)
    }
    

    You can use it for do something after reload data:

    [UIView animateWithDuration:0 animations:^{
        [self.contentTableView reloadData];
    } completion:^(BOOL finished) {
        _isUnderwritingUpdate = NO;
    }];
    

    Try setting delays:

    [_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2];
    [_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];