200

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)

Cœur
  • 32,421
  • 21
  • 173
  • 232
Alan
  • 8,681
  • 12
  • 48
  • 90
  • Possible duplicate of this question: http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview – pmk Apr 18 '13 at 18:54
  • 2
    best solution I have seen. http://stackoverflow.com/questions/1483581/get-notified-when-uitableview-has-finished-asking-for-data#21581834 – Khaled Annajar Jun 22 '15 at 13:30
  • My answer for the following might help you, http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview/40278527#40278527 – Suhas Aithal Oct 27 '16 at 07:24
  • Try my answer here - http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview/40278527#40278527 – Suhas Aithal Oct 27 '16 at 07:43

18 Answers18

301

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

rob mayoff
  • 342,380
  • 53
  • 730
  • 766
  • 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. – matt Apr 18 '13 at 00:54
  • 3
    @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!! – Mr. T May 29 '13 at 00:56
  • 9
    Method 2 did not work for me for some unknown reason, but chose the first method instead. – Raj Pawan Gumdal Aug 20 '14 at 21:00
  • Thank you for this. the layoutIfNeeded call did it for me, fixed some crazy timing issues I was having in some textviews – Rich Fox Nov 12 '14 at 17:19
  • Both methods didn't work for me. I'm using iOS 9 if that helps – sosale151 Dec 15 '15 at 16:51
  • 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. – Marcio Cruz Feb 07 '16 at 00:53
  • 5
    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. – Tyler Sheaffer Sep 22 '16 at 20:45
  • 3
    Agree with `dispatch_async(dispatch_get_main_queue())` not always working. Seeing random results here. – Vojto Jan 26 '17 at 07:02
  • @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. – xaphod Dec 08 '17 at 04:51
  • 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... – Honey Dec 30 '17 at 08:02
  • 1
    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. – rob mayoff Dec 30 '17 at 18:02
  • 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? – Honey Dec 31 '17 at 00:59
  • 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? – Honey Aug 16 '18 at 21:26
  • I removed most of your edit because it made claims that I have not personally verified. – rob mayoff Aug 16 '18 at 21:46
  • 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) }``` – kakashy Mar 19 '19 at 02:48
  • Force reload didn't work but putting it on the main queue worked. – ScottyBlades Jun 07 '19 at 17:40
109

Swift:

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

...somewhere later...

tableView.reloadData {
    print("done")
}

Objective-C:

[UIView animateWithDuration:0 animations:^{
    [myTableView reloadData];
} completion:^(BOOL finished) {
    //Do something after that...
}];
Abhirajsinh Thakore
  • 1,608
  • 1
  • 9
  • 21
Aviel Gross
  • 8,880
  • 2
  • 47
  • 58
  • 18
    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. – seo May 16 '14 at 23:19
  • 1
    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. – Chrstpsln Dec 22 '14 at 10:41
  • @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. – Gobe Feb 24 '16 at 21:57
  • This solution is awesome. Thanks!! <3 – Naval Hasan Mar 25 '21 at 03:41
58

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

Denis Kreshikhin
  • 6,887
  • 6
  • 45
  • 75
kolaworld
  • 681
  • 6
  • 4
  • I also works if you're performing custom reloading, like manually inserting, deleting or moving rows in table view, within `beginUpdates` and `endUpdates` calls. – Darrarski May 11 '18 at 09:42
  • I believe this is actually the modern solution. indeed it's the common pattern in iOS, example ... https://stackoverflow.com/a/47536770/294884 – Fattie Aug 16 '18 at 03:00
  • 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! @Fattie – Honey Aug 17 '18 at 13:03
  • 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. – Leon Sep 23 '20 at 15:46
  • 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. – JTODR Oct 13 '20 at 13:47
33

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

Honey
  • 24,125
  • 14
  • 123
  • 212
Tyler Sheaffer
  • 1,753
  • 1
  • 13
  • 16
  • 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 – Jon Vogel Apr 21 '17 at 00:28
  • 2
    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. – Mark Bourke Jun 16 '17 at 14:00
  • why can't I use this for UITableView? it's showing no visible interface. I imported the header file also but still same – Julfikar Nov 23 '17 at 03:05
  • 2
    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. – Tyler Sheaffer Nov 24 '17 at 07:24
  • 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?! – Honey Dec 31 '17 at 01:15
  • @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. – Tyler Sheaffer Jan 01 '18 at 18:11
  • @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://stackoverflow.com/a/47536770/294884 . I think (but I'm not totally certain) that these days that is the normal, everyday solution to this problem. – Fattie Aug 16 '18 at 03:00
30

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
}
Honey
  • 24,125
  • 14
  • 123
  • 212
Shawn Aukstak
  • 726
  • 6
  • 7
  • 1
    nice! small nit-pick, you can remove the `if let` by saying `reloadDataCompletionBlock?()` which will call iff not nil – Tyler Sheaffer Dec 17 '16 at 06:42
  • No luck with this one in my situation on ios9 – Matjan Feb 05 '17 at 20:20
  • ```self.reloadDataCompletionBlock? { completion() }``` should have been ```self.reloadDataCompletionBlock?()``` – emem May 26 '17 at 00:46
  • How do I handle resize of table view height? I was previously calling tableView.beginUpdates() tableView.layoutIfNeeded() tableView.endUpdates() – Parth Tamane Nov 15 '19 at 05:17
10

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)
}
Womble
  • 3,974
  • 2
  • 26
  • 39
6

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];
XJones
  • 21,828
  • 10
  • 63
  • 81
  • I updated my questions. Do you know why my NSLogs would output like this? – Alan Apr 18 '13 at 14:42
  • 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? – XJones Apr 18 '13 at 16:19
  • 8
    `reloadData` is not synchronous. It used to be - see this answer: http://stackoverflow.com/a/16071589/193896 – bendytree May 24 '13 at 00:12
  • 1
    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. – XJones May 24 '13 at 00:42
  • 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. – strange May 23 '14 at 17:28
  • 1
    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. – XJones May 23 '14 at 21:17
  • 3
    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? – fatuhoku Jul 12 '14 at 12:49
  • 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. – XJones Jul 12 '14 at 16:36
  • Is there a way to know when the visual layout changes completed for a `UITableView`? – oyalhi Jan 06 '16 at 08:53
  • Same findings here: It's not synchronous – drct Jan 23 '17 at 14:57
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;
}
malhal
  • 17,500
  • 6
  • 94
  • 112
  • @Fattie 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. – Cœur Jul 11 '18 at 03:59
  • 1
    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. – malhal Jul 12 '18 at 21:53
3

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
}}
Asha Antony
  • 431
  • 5
  • 8
2

In Swift 3.0 + we can create a an extension for UITableView with a escaped Closure like below :

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

And Use it like Below, wherever you want :

Your_Table_View.reloadData {
   print("reload done")
 }

hope this will help to someone. cheers!

Chanaka Anuradh Caldera
  • 4,280
  • 7
  • 34
  • 61
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

Other Solution is you can subclass UITableView

Duck
  • 32,792
  • 46
  • 221
  • 426
Shashi3456643
  • 1,991
  • 15
  • 20
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 the interface builder:

enter image description here

Sam
  • 4,036
  • 1
  • 36
  • 39
  • 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. – Yash Bedi Aug 24 '18 at 11:03
  • 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. – Sam Nov 07 '18 at 20:50
0

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()
bauerMusic
  • 4,115
  • 4
  • 31
  • 41
0

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

Vasily Bodnarchuk
  • 19,860
  • 8
  • 111
  • 113
-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.

Phontaine Judd
  • 308
  • 5
  • 14
Nirbhay Singh
  • 675
  • 1
  • 6
  • 14
-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
})
Umair
  • 956
  • 9
  • 11
-2

You can use it for do something after reload data:

[UIView animateWithDuration:0 animations:^{
    [self.contentTableView reloadData];
} completion:^(BOOL finished) {
    _isUnderwritingUpdate = NO;
}];
Vit
  • 900
  • 1
  • 9
  • 20
-20

Try setting delays:

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