How can I tell when UITableView has completed ReloadData?
Asked Answered
A

18

219

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.

It's weird 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)

Alburnum answered 17/4, 2013 at 22:41 Comment(4)
Possible duplicate of this question: #4164079Dierolf
best solution I have seen. #1484081Pleistocene
My answer for the following might help you, #4164079Tiercel
Try my answer here - #4164079Tiercel
A
318

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

Almemar answered 17/4, 2013 at 22:49 Comment(16)
I like the second way. I'm a huge fan of delayed performance for situations like this. And GCD (dispatch_async) makes it so clean and easy.Bassarisk
@rob, depending on how big your table data source is, you can animate going to the bottom of the tableview in the same run loop. If you try your test code with a huge table, your trick of using GCD to delay scrolling until the next run loop will work, whereas immediately scrolling will fail. But anyways, thanks for this trick!!Vorster
Method 2 did not work for me for some unknown reason, but chose the first method instead.Godderd
Thank you for this. the layoutIfNeeded call did it for me, fixed some crazy timing issues I was having in some textviewsSherilyn
Both methods didn't work for me. I'm using iOS 9 if that helpsPiquant
I had a problem with dynamic row heights where it would no scroll to the full end of the tableview when doing it on the viewDidLoad. Using the second solution worked for me.Consist
the dispatch_async(dispatch_get_main_queue()) method 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. I'll post an answer that worked for me below.Lowdown
Agree with dispatch_async(dispatch_get_main_queue()) not always working. Seeing random results here.Hochman
@TylerSheaffer and others that relying on the threading model is not a good idea. I can consistently make that (dispatching async) method NOT work when i'm in a child view controller (aka container view controller). Forcing the layout with layoutIfNeeded() works well for me.Ample
If I just do reloadData on the main thread, then since main thread is synchronous doesn't that mean whatever that comes after it will happen after it? or even though its on main thread, reload data itself always ends up on background thread, so you need some other mechanism, hence his question...Pentathlon
The main thread runs an NSRunLoop. A run loop has different phases, and you can schedule a callback for a specific phase (using a CFRunLoopObserver). UIKit schedules layout to happen during a later phase, after your event handler returns.Almemar
Thanks. 1) What is your final answer? If you put it on main thread, wouldn't it wait till reloadData is finished? Yes or No? I think the answer is yes, but the layout (which is another thing) is scheduled for later 2) after your event handler returns means what moment/event exactly? Are you talking about the reloadData event? Do you mean the layout of the views/UI is scheduled to happen later...but we need it now? so this question... 3) I've read about runloops before, but still don't have good knowledge on it. Is there specific keyword that I could search and learn what you just said?Pentathlon
I've made an edit to your answer, but tbh after making the edit, I'm a little confused myself if it's correct or not. Can you correct the edit rather than just rolling it back?Pentathlon
I removed most of your edit because it made claims that I have not personally verified.Almemar
Guys in my case I need to get the cell after reload I use the following code DispatchQueue.main.async { let indexPath = IndexPath(row: 5, section: 0) let defaultCell = self.tableView.dequeueReusableCell(withIdentifier: CreditCardTableViewCell.cellIdentifier, for: indexPath) self.tableView.scrollRectToVisible(defaultCell.frame, animated: true) }Campania
Force reload didn't work but putting it on the main queue worked.Wintertime
W
116

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...
}];
Watt answered 15/9, 2013 at 13:17 Comment(4)
That is equivalent to dispatching something on the main thread in the "near future". It's likely you're just seeing the table view render the objects before the main thread deques the completion block. It's not advised to do this kind of hack in the first place, but in any case, you should use dispatch_after if you're going to attempt this.Gordie
The Rob's solution is good but doesn't work if there are no rows in the tableview. Aviel's solution has the advantage to work even when the table contains no lines but only sections.Baliol
@Christophe As of now, I was able to use Rob's update in a table view without any rows by overriding in my Mock view controller the tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int method and inserting in my override whatever I wanted to notify that the reload had finished.Afar
This is great and works with collection views as wellDiskson
U
65

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

Unscientific answered 1/4, 2017 at 21:29 Comment(8)
👍 I also works if you're performing custom reloading, like manually inserting, deleting or moving rows in table view, within beginUpdates and endUpdates calls.Schnorkle
I believe this is actually the modern solution. indeed it's the common pattern in iOS, example ... https://mcmap.net/q/120680/-animation-end-callback-for-calayerKanter
I tried this. I got a very odd behavior. My tableview correctly shows two headerViews. Inside the setCompletionBlock my numberOfSections shows 2 ...so far so good. Yet if inside setCompletionBlock I do tableView.headerView(forSection: 1) it returns nil!!! hence I think this block is either happening before the reload or captures something before or I'm doing something wrong. FYI I did try Tyler's answer and that worked! @KanterPentathlon
I am using this it for scrolling to the top of the table once the the table data has reloaded. It works great in most cases but has an offset if the height of the top row is different between before and after the reload. This would seem to correlate with rob mayoff's findings.Matterhorn
This was very helpful thanks! I had an issue where calling reloadData() on my tableview would sometimes trigger the tableview's scrollViewDidScroll() method. I was able to block the scrollViewDidScroll() from being called until the completion block was finished.Swept
It works but I didn't understand why... reloadData() is not async...Suave
@MattiaDucci Yes, it isPeacoat
This is the right way to do it. Don't mess with dispatch_async or UIView.animateStatius
L
44

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

Lowdown answered 22/9, 2016 at 20:49 Comment(8)
This is the only correct way. Added it to my project because I needed the final frames of some cells for animation purposes. I also added and edit for Swift. Hope you don't mind 😉Allspice
After you call the block in layoutSubviews it should be set to nil as subsequent calls to layoutSubviews, not necessarily due to reloadData being called, will result in the block being executed as there is a strong reference being held, which is not the desired behaviour.Cinerary
why can't I use this for UITableView? it's showing no visible interface. I imported the header file also but still sameSaluki
An addendum to this answer is that it's possible to clobber the existing callback if there's just one, meaning multiple callers will have a race-condition. The solution is to make reloadDataCompletionBlock an array of blocks and iterate over them on execution and empty the array after that.Lowdown
1) isn't this equivalent to Rob's first answer ie to use layoutIfNeeded? 2) why did you mention iOS 10, does it not work on iOS 9?!Pentathlon
@Honey it works on iOS 9 as well, just wanted to specify that I hadn't rigorously tested it on anything except iOS 9 when I posted the answer. It's currently used for a critical callback on our production app with 10s of thousands of iOS 9 (and 11) users and it's working perfectly.Lowdown
@honey i >think< the best solution these days is to very simply use a completion block. (Kola's answer below.) It's rather like when you need to know when a CA animation has finished ... https://mcmap.net/q/120680/-animation-end-callback-for-calayer . I think (but I'm not totally certain) that these days that is the normal, everyday solution to this problem.Kanter
The only reliable solution! Works fine on iOS 14.5.Beyrouth
C
34

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
}
Chicken answered 30/9, 2016 at 18:40 Comment(4)
nice! small nit-pick, you can remove the if let by saying reloadDataCompletionBlock?() which will call iff not nil 💥Lowdown
No luck with this one in my situation on ios9Kabuki
self.reloadDataCompletionBlock? { completion() } should have been self.reloadDataCompletionBlock?()Clemente
How do I handle resize of table view height? I was previously calling tableView.beginUpdates() tableView.layoutIfNeeded() tableView.endUpdates()Doubledealing
C
12

And a UICollectionView version, based on kolaworld's answer.

It needs testing. It works so far on iOS 9.2 and 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)
}
Chaille answered 13/11, 2017 at 2:29 Comment(0)
B
7

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];
Boxberry answered 17/4, 2013 at 23:40 Comment(10)
I updated my questions. Do you know why my NSLogs would output like this?Alburnum
You say it is returning # of sections = 0. That means the datasource was empty the last time the table was reloaded and you are calling numberOfRowsInSection: with -1 as the section index. Is that what you meant?Boxberry
reloadData is not synchronous. It used to be - see this answer: https://mcmap.net/q/118560/-how-can-i-tell-when-uitableview-has-completed-reloaddataDistiller
It is synchronous. It's very easy to test and see this with a sample app. You linked to @rob's answer in this question. If you read his update at the bottom, he has verified this as well. Perhaps you are talking about the visual layout changes. It is true that the tableView is not visibly updated synchronously but the data is. That's why the values the OP needs are correct immediately after reloadData returns.Boxberry
No it is not synchronous. It may be for certain cases, but in my test case I can see it isn't. I call reloadData in viewWillAppear and only after the view has fully appeared does it seem to reload the data, while everything else 'after that line' gets called and executed.Porism
You may be confused about what is expected to happen in reloadData. Use my test case in viewWillAppear accept for the scrollToRowAtIndexPath: line b/c that is meaningless if the tableView isn't displayed. You will see that reloadData did update the data cached in the tableView instance and that reloadData is synchronous. If you are referring to other tableView delegate methods called when the tableView is being layout out those won't get called if the tableView is not displayed. If I am misunderstanding your scenario please explain.Boxberry
What fun times. It's 2014, and there are arguments over whether some method is synchronous and asynchronous or not. Feels like guesswork. All implementation detail is completely opaque behind that method name. Isn't programming great?Dilemma
the point here is not about guessing the internals of framework implementations. the question is really about the integrity of the tableView after reloadData. the docs and observed behavior are clear.Boxberry
Is there a way to know when the visual layout changes completed for a UITableView?Spermatogonium
Same findings here: It's not synchronousRutty
S
5

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;
}
Shaunta answered 18/10, 2016 at 18:17 Comment(2)
@Kanter it's unclear if you mean it as a positive comment or a negative comment. But I saw you've commented another answer as "this seems to be the best solution!", so I guess that relatively speaking, you do not consider this solution to be the best.Hydra
Relying on a side affect of a fake animation? No way is that a good idea. Learn perform selector or GCD and do it properly. Btw there is now a table loaded method you could just use that if you don’t mind using a private protocol which is prob fine because it’s the framework calling your code rather than other way around.Shaunta
G
4

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
}}
Gersham answered 2/6, 2015 at 13:20 Comment(0)
R
1

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.

Another solution is you can subclass UITableView.

Radioluminescence answered 2/10, 2014 at 15:14 Comment(0)
T
1

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 Interface Builder:

Enter image description here

Telly answered 8/4, 2017 at 18:58 Comment(2)
this works but the problem is the method gets hit every time its done a loading a single Cell, NOT THE WHOLE TABLE VIEW RELOAD, so clearly this answer isn't in respect to the question asked.Paddle
True, it gets called more than once, but not on every cell. So you could listen to the first delegate and ignore the rest until you call reloadData again.Telly
C
1

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

Currey answered 28/6, 2019 at 19:40 Comment(0)
K
0

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.

Kraus answered 18/6, 2021 at 7:55 Comment(0)
S
0

I've regularly encountered the same issue and it happened even with only a couple of rows in the table (and I ended up adding some of the contrived workarounds others have mentioned).

However, the last note in Rob's answer (saying that the posted code actually works fine for him) got me thinking. I noticed that in my particular case the problem only happened when the reload call was issued from within an observeValueForKeyPath() method.

The solution was to simply wrap the entire logic in dispatch_async(dispatch_get_main_queue(), ^{...}) inside the observeValueForKeyPath() method.

Note: I did not test this solution with very large datasets.

Stovall answered 30/7, 2023 at 20:27 Comment(0)
J
-1

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.

Jackiejackinoffice answered 14/4, 2019 at 5:47 Comment(0)
L
-1

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
})
Larder answered 17/6, 2020 at 18:31 Comment(0)
I
-2

You can use it for do something after reload data:

[UIView animateWithDuration:0 animations:^{
    [self.contentTableView reloadData];
} completion:^(BOOL finished) {
    _isUnderwritingUpdate = NO;
}];
Irrelievable answered 29/10, 2015 at 13:58 Comment(0)
C
-20

Try setting delays:

[_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2];
[_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];
Crook answered 18/5, 2014 at 20:49 Comment(1)
This is dangerous. What if it takes longer to reload than your delay?Hafiz

© 2022 - 2024 — McMap. All rights reserved.