How to tell when UITableView has completed ReloadData?

Objective CUitableviewReloaddata

Objective C Problem Overview


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

I originally had

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

Objective C Solutions


Solution 1 - Objective C

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:numberOfSections: and 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: or tableView:headerViewForSection until the layout phase, which happens by default when you return control to the run loop.

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

Solution 2 - Objective C

Swift:

extension UITableView {
    func reloadData(completion:@escaping ()->()) {
        UIView.animate(withDuration: 0, animations: reloadData)
            { _ in completion() }
    } 
}

// ...somewhere later...

tableView.reloadData {
    print("done")
}

Objective-C:

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

Solution 3 - Objective C

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

Solution 4 - Objective C

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

See here for a Swift version of this answer

Solution 5 - Objective C

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
    self.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
      self.reloadData()
  }
}

Example Usage:

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

Solution 6 - Objective C

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

Solution 7 - Objective C

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

Solution 8 - Objective C

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

Solution 9 - Objective C

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

Solution 10 - Objective C

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

Solution 11 - Objective C

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

Solution 12 - Objective C

Just to offer another approach, based on the idea of the completion being the 'last visible' cell to be sent to cellForRow.

// Will be set when reload is called
var lastIndexPathToDisplay: IndexPath?

typealias ReloadCompletion = ()->Void

var reloadCompletion: ReloadCompletion?

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

    // Setup cell
    
    if indexPath == self.lastIndexPathToDisplay {

        self.lastIndexPathToDisplay = nil
        
        self.reloadCompletion?()
        self.reloadCompletion = nil
    }

    // Return cell
...

func reloadData(completion: @escaping ReloadCompletion) {
    
    self.reloadCompletion = completion
    
    self.mainTable.reloadData()
    
    self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
}

One possible issue is: If reloadData() has finished before the lastIndexPathToDisplay was set, the 'last visible' cell will be displayed before lastIndexPathToDisplay was set and the completion will not be called (and will be in 'waiting' state):

self.mainTable.reloadData()

// cellForRowAt could be finished here, before setting `lastIndexPathToDisplay`

self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last

If we reverse, we could end up with completion being triggered by scrolling before reloadData().

self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last

// cellForRowAt could trigger the completion by scrolling here since we arm 'lastIndexPathToDisplay' before 'reloadData()'

self.mainTable.reloadData()

Solution 13 - Objective C

Details

  • Xcode Version 10.2.1 (10E1001), Swift 5

Solution

import UIKit

// MARK: - UITableView reloading functions

protocol ReloadCompletable: class { func reloadData() }

extension ReloadCompletable {
    func run(transaction closure: (() -> Void)?, completion: (() -> Void)?) {
        guard let closure = closure else { return }
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        closure()
        CATransaction.commit()
    }
    
    func run(transaction closure: (() -> Void)?, completion: ((Self) -> Void)?) {
        run(transaction: closure) { [weak self] in
            guard let self = self else { return }
            completion?(self)
        }
    }
    
    func reloadData(completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadData() }, completion: closure)
    }
}

// MARK: - UITableView reloading functions

extension ReloadCompletable where Self: UITableView {
    func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadRows(at: indexPaths, with: animation) }, completion: closure)
    }
    
    func reloadSections(_ sections: IndexSet, with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadSections(sections, with: animation) }, completion: closure)
    }
}

// MARK: - UICollectionView reloading functions

extension ReloadCompletable where Self: UICollectionView {
    
    func reloadSections(_ sections: IndexSet, completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadSections(sections) }, completion: closure)
    }
    
    func reloadItems(at indexPaths: [IndexPath], completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadItems(at: indexPaths) }, completion: closure)
    }
}

Usage

> UITableView

// Activate
extension UITableView: ReloadCompletable { }

// ......
let tableView = UICollectionView()

// reload data
tableView.reloadData { tableView in print(collectionView) }

// or
tableView.reloadRows(at: indexPathsToReload, with: rowAnimation) { tableView in print(tableView) }

// or
tableView.reloadSections(IndexSet(integer: 0), with: rowAnimation) { _tableView in print(tableView) }

> UICollectionView

// Activate
extension UICollectionView: ReloadCompletable { }

// ......
let collectionView = UICollectionView()

// reload data
collectionView.reloadData { collectionView in print(collectionView) }

// or
collectionView.reloadItems(at: indexPathsToReload) { collectionView in print(collectionView) }

// or
collectionView.reloadSections(IndexSet(integer: 0)) { collectionView in print(collectionView) }

Full sample

> Do not forget to add the solution code here

import UIKit

class ViewController: UIViewController {
    
    private weak var navigationBar: UINavigationBar?
    private weak var tableView: UITableView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupNavigationItem()
        setupTableView()
    }
}
// MARK: - Activate UITableView reloadData with completion functions

extension UITableView: ReloadCompletable { }

// MARK: - Setup(init) subviews

extension ViewController {

    private func setupTableView() {
        guard let navigationBar = navigationBar else { return }
        let tableView = UITableView()
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.dataSource = self
        self.tableView = tableView
    }
    
    private func setupNavigationItem() {
        let navigationBar = UINavigationBar()
        view.addSubview(navigationBar)
        self.navigationBar = navigationBar
        navigationBar.translatesAutoresizingMaskIntoConstraints = false
        navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        let navigationItem = UINavigationItem()
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "all", style: .plain, target: self, action: #selector(reloadAllCellsButtonTouchedUpInside(source:)))
        let buttons: [UIBarButtonItem] = [
                                            .init(title: "row", style: .plain, target: self,
                                                  action: #selector(reloadRowButtonTouchedUpInside(source:))),
                                            .init(title: "section", style: .plain, target: self,
                                                  action: #selector(reloadSectionButtonTouchedUpInside(source:)))
                                            ]
        navigationItem.leftBarButtonItems = buttons
        navigationBar.items = [navigationItem]
    }
}

// MARK: - Buttons actions

extension ViewController {
    
    @objc func reloadAllCellsButtonTouchedUpInside(source: UIBarButtonItem) {
        let elementsName = "Data"
        print("-- Reloading \(elementsName) started")
        tableView?.reloadData { taleView in
            print("-- Reloading \(elementsName) stopped \(taleView)")
        }
    }
    
    private var randomRowAnimation: UITableView.RowAnimation {
        return UITableView.RowAnimation(rawValue: (0...6).randomElement() ?? 0) ?? UITableView.RowAnimation.automatic
    }
    
    @objc func reloadRowButtonTouchedUpInside(source: UIBarButtonItem) {
        guard let tableView = tableView else { return }
        let elementsName = "Rows"
        print("-- Reloading \(elementsName) started")
        let indexPathToReload = tableView.indexPathsForVisibleRows?.randomElement() ?? IndexPath(row: 0, section: 0)
        tableView.reloadRows(at: [indexPathToReload], with: randomRowAnimation) { _tableView in
            //print("-- \(taleView)")
            print("-- Reloading \(elementsName) stopped in \(_tableView)")
        }
    }
    
    @objc func reloadSectionButtonTouchedUpInside(source: UIBarButtonItem) {
        guard let tableView = tableView else { return }
        let elementsName = "Sections"
        print("-- Reloading \(elementsName) started")
        tableView.reloadSections(IndexSet(integer: 0), with: randomRowAnimation) { _tableView in
            //print("-- \(taleView)")
            print("-- Reloading \(elementsName) stopped in \(_tableView)")
        }
    }
}

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int { return 1 }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 20 }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = "\(Date())"
        return cell
    }
}

Results

enter image description here

Solution 14 - Objective C

If you reload data when viewDidLoad, you can put your code into the viewDidLayoutSubviews method. But you should be careful that viewDidLayoutSubviews may be called for multiple times.

Solution 15 - Objective C

Try this:

tableView.backgroundColor = .black

tableView.reloadData()

DispatchQueue.main.async(execute: {

    tableView.backgroundColor = .green

})

The tableView color will changed from black to green only after the reloadData() function completes.

Solution 16 - Objective C

Creating a reusable extension of CATransaction:

public extension CATransaction {
    static func perform(method: () -> Void, completion: @escaping () -> Void) {
        begin()
        setCompletionBlock {
            completion()
        }
        method()
        commit()
    }
}

Now creating an extension of UITableView that would use CATransaction's extension method:

public extension UITableView {
    func reloadData(completion: @escaping (() -> Void)) {
       CATransaction.perform(method: {
           reloadData()
       }, completion: completion)
    }
}

Usage:

tableView.reloadData(completion: {
    //Do the stuff
})

Solution 17 - Objective C

You can use it for do something after reload data:

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

Solution 18 - Objective C

Try setting delays:

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

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
QuestionAlanView Question on Stackoverflow
Solution 1 - Objective Crob mayoffView Answer on Stackoverflow
Solution 2 - Objective CAviel GrossView Answer on Stackoverflow
Solution 3 - Objective CkolaworldView Answer on Stackoverflow
Solution 4 - Objective CTyler SheafferView Answer on Stackoverflow
Solution 5 - Objective CShawn AukstakView Answer on Stackoverflow
Solution 6 - Objective CWombleView Answer on Stackoverflow
Solution 7 - Objective CXJonesView Answer on Stackoverflow
Solution 8 - Objective CmalhalView Answer on Stackoverflow
Solution 9 - Objective CAsha AntonyView Answer on Stackoverflow
Solution 10 - Objective CShashi3456643View Answer on Stackoverflow
Solution 11 - Objective CSamView Answer on Stackoverflow
Solution 12 - Objective CbauerMusicView Answer on Stackoverflow
Solution 13 - Objective CVasily BodnarchukView Answer on Stackoverflow
Solution 14 - Objective CMr. MíngView Answer on Stackoverflow
Solution 15 - Objective CNirbhay SinghView Answer on Stackoverflow
Solution 16 - Objective CUmairView Answer on Stackoverflow
Solution 17 - Objective CVitView Answer on Stackoverflow
Solution 18 - Objective CJakeView Answer on Stackoverflow