101

I have to do some operation whenever UICollectionView has been loaded completely, i.e. at that time all the UICollectionView's datasource / layout methods should be called. How do I know that?? Is there any delegate method to know UICollectionView loaded status?

kuzyn
  • 1,657
  • 13
  • 28
Jirune
  • 2,180
  • 3
  • 20
  • 19

20 Answers20

165

This worked for me:

[self.collectionView reloadData];
[self.collectionView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// collection-view finished reload
                              }];

Swift 4 syntax:

collectionView.reloadData()
collectionView.performBatchUpdates(nil, completion: {
    (result) in
    // ready
})
Zaporozhchenko Oleksandr
  • 3,802
  • 3
  • 18
  • 36
myeyesareblind
  • 3,136
  • 3
  • 14
  • 8
  • 2
    Worked for me on iOS 9 – Magoo Apr 26 '16 at 11:57
  • 6
    @G.Abhisek Errrr, sure.... what do you need explaining? The first line `reloadData` refreshes the `collectionView` so it draws upon it's datasource methods again... the `performBatchUpdates` has a completion block attached so if both are performed on the main thread you know any code that put in place of `/// collection-view finished reload` will execute with a fresh and laid out `collectionView` – Magoo May 12 '16 at 08:42
  • 9
    Does not worked for me when the collectionView had dynamic sized cells – ingaham Aug 08 '16 at 11:01
  • 2
    This method is perfect solution without any headache. – arjavlad Mar 07 '17 at 09:04
  • 2
    @ingaham This works perfectly for me with dynamic sized cells, Swift 4.2 IOS 12 – Romulo BM Jan 04 '19 at 15:35
  • @ingaham [self.collectionView insertItemsAtIndexPaths:indexSet]; dispatch_async(dispatch_get_main_queue(), ^{ [self.collectionView scrollToItemsAtIndexPaths:indexSet scrollPosition:NSCollectionViewScrollPositionBottom]; }); – prabhu Feb 14 '19 at 06:57
  • Works for me in iOS 11. Thanks. – userx Mar 13 '19 at 05:19
  • 1
    Works for me with dynamic sized cells (AutoLayout) on iOS 13.4 – Skoua Apr 13 '20 at 10:47
63
// In viewDidLoad
[self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionOld context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary  *)change context:(void *)context
{
    // You will get here when the reloadData finished 
}

- (void)dealloc
{
    [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];
}
klefevre
  • 8,134
  • 7
  • 37
  • 68
Cullen SUN
  • 3,417
  • 3
  • 30
  • 32
30

It's actually rather very simple.

When you for example call the UICollectionView's reloadData method or it's layout's invalidateLayout method, you do the following:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
});

dispatch_async(dispatch_get_main_queue(), ^{
    //your stuff happens here
    //after the reloadData/invalidateLayout finishes executing
});

Why this works:

The main thread (which is where we should do all UI updates) houses the main queue, which is serial in nature, i.e. it works in the FIFO fashion. So in the above example, the first block gets called, which has our reloadData method being invoked, followed by anything else in the second block.

Now the main thread is blocking as well. So if you're reloadData takes 3s to execute, the processing of the second block will be deferred by those 3s.

dezinezync
  • 2,364
  • 18
  • 14
  • 2
    I actually just tested this, The block is getting called before all of my other delegate methods. So it doesn't work? – taylorcressy Aug 05 '14 at 23:07
  • @taylorcressy hey, I've updated the code, something that I'm using now in 2 production apps. Also included the explanation. – dezinezync Aug 06 '14 at 05:32
  • Doesn't work for me. When I call reloadData, the collectionView requests cells after the execution of the second block. So I'm having the same problem as @taylorcressy seem to have. – Raphael Sep 17 '14 at 17:27
  • 1
    Can you try putting the second block call within the first? Unverified, but might work. – dezinezync Sep 18 '14 at 06:03
  • I don't think this is the correct solution. I think it may only work some of the time because you don't know what's happening under the hood of the `reloadData`. It might be, in some instances, the second async block is added to the main queue before the code in `reloadData` decides to put something on the main thread. – larromba Aug 11 '15 at 13:48
  • @larromba AFAIK, all operations that happen inside `reloadData` are synchronous. Therefore, any code you enqueue on the main thread after the `reloadData` invocation get triggered after it has finished. – dezinezync Aug 12 '15 at 14:48
  • @dezinezync ok cool. Would be interesting to know then why some people found that this doesn't work then – larromba Aug 12 '15 at 19:17
  • @larromba yes, indeed. Only code examples would help in such situations. – dezinezync Aug 13 '15 at 13:57
  • This does not work for me. I have data coming in to the collection view and the first section I do a reload data, the subsequent data I do a insert section. The problem happens when the two calls are too close together. Perhaps reload data is not syncronous – Ryan Heitner Aug 17 '15 at 15:23
  • @ryan: have you tried wrapping both inside the block? – dezinezync Aug 18 '15 at 16:30
  • What I did was to put a @synchronised on the calling method I also put a flag in the completion blocks of collectionView performBatchUpdates: and ignore my insert and use reload if the finished flag had not been set. This seems to work 99% of the time. – Ryan Heitner Aug 18 '15 at 18:25
  • The docs say for performBatchUpdates: A completion handler block to execute when all of the operations are finished. This block takes a single Boolean parameter that contains the value YES if all of the related animations completed successfully or NO if they were interrupted. This parameter may be nil. So in theory the flag should work – Ryan Heitner Aug 18 '15 at 18:28
  • @ryan: can you please post this sample somewhere so I could digg into it as well? I'm intrigued by this. – dezinezync Aug 18 '15 at 19:39
  • Depends what you want to know... ContentSize might not be accurate at this time – Magoo Apr 26 '16 at 11:53
  • 6
    ```reloadData``` now enqueues the loading of the cells on the main thread (even if called from the main thread). So the cells aren't actually loaded (```cellForRowAtIndexPath:``` isn't called) after ```reloadData``` returns. Wrapping code you want executed after your cells are loaded in ```dispatch_async(dispatch_get_main...``` and calling that after your call to ```reloadData``` will achieve the desired result. – Tylerc230 Jun 02 '16 at 23:38
  • @taylorcressy As for why this doesn't work. Follow the comments starting from [here](https://stackoverflow.com/questions/16071503/how-to-tell-when-uitableview-has-completed-reloaddata/16071589#comment83034337_16071589). The gist of it is: *"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**"* . But also make sure you read the answer as well. – Honey Dec 31 '17 at 03:28
13

Just to add to a great @dezinezync answer:

Swift 3+

collectionView.collectionViewLayout.invalidateLayout() // or reloadData()
DispatchQueue.main.async {
    // your stuff here executing after collectionView has been layouted
}
nikans
  • 2,143
  • 25
  • 30
7

A different approaching using RxSwift/RxCocoa:

        collectionView.rx.observe(CGSize.self, "contentSize")
            .subscribe(onNext: { size in
                print(size as Any)
            })
            .disposed(by: disposeBag)
Chinh Nguyen
  • 405
  • 5
  • 11
  • 1
    I'm liking this. `performBatchUpdates` wasn't working in my case, this one does it. Cheers! – Zoltán Feb 08 '19 at 14:04
  • @MichałZiobro, Can you update your code somewhere? The method should be called only when contentSize got changed? So, unless you change it on every scroll, this should work! – Chinh Nguyen May 29 '19 at 12:22
  • this working so cool for me, it not only scroll to needed cell, but keep scrolling back(coz it works on contentsize changes constantly), I guess this is not a bug, but a feature xD. However, this not really clear solution – Zaporozhchenko Oleksandr May 27 '21 at 09:48
6

Do it like this:

       UIView.animateWithDuration(0.0, animations: { [weak self] in
                guard let strongSelf = self else { return }

                strongSelf.collectionView.reloadData()

            }, completion: { [weak self] (finished) in
                guard let strongSelf = self else { return }

                // Do whatever is needed, reload is finished here
                // e.g. scrollToItemAtIndexPath
                let newIndexPath = NSIndexPath(forItem: 1, inSection: 0)
                strongSelf.collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
        })
Darko
  • 8,619
  • 7
  • 28
  • 42
4

As dezinezync answered, what you need is to dispatch to the main queue a block of code after reloadData from a UITableView or UICollectionView, and then this block will be executed after cells dequeuing

In order to make this more straight when using, I would use an extension like this:

extension UICollectionView {
    func reloadData(_ completion: @escaping () -> Void) {
        reloadData()
        DispatchQueue.main.async { completion() }
    }
}

It can be also implemented to a UITableView as well

4

Try forcing a synchronous layout pass via layoutIfNeeded() right after the reloadData() call. Seems to work for both UICollectionView and UITableView on iOS 12.

collectionView.reloadData()
collectionView.layoutIfNeeded() 

// cellForItem/sizeForItem calls should be complete
completion?()
tyler
  • 2,665
  • 1
  • 19
  • 28
3

I just did the following to perform anything after collection view is reloaded. You can use this code even in API response.

self.collectionView.reloadData()

DispatchQueue.main.async {
   // Do Task after collection view is reloaded                
}
Asad Jamil
  • 160
  • 8
3

The best solution I have found so far is to use CATransaction in order to handle completion.

Swift 5:

CATransaction.begin()
CATransaction.setCompletionBlock {
    // UICollectionView is ready
}

collectionView.reloadData()

CATransaction.commit()

Updated: The above solution seems to work in some cases and in some cases it doesn't. I ended up using the accepted answer and it's definitely the most stable and proved way. Here is Swift 5 version:

private var contentSizeObservation: NSKeyValueObservation?
contentSizeObservation = collectionView.observe(\.contentSize) { [weak self] _, _ in
      self?.contentSizeObservation = nil
      completion()
}

collectionView.reloadData()
2

SWIFT 5

override func viewDidLoad() {
    super.viewDidLoad()
    
    // "collectionViewDidLoad" for transitioning from product's cartView to it's cell in that view
    self.collectionView?.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: nil)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let observedObject = object as? UICollectionView, observedObject == self.collectionView {
        print("collectionViewDidLoad")
        self.collectionView?.removeObserver(self, forKeyPath: "contentSize")
    }
}
AndrewK
  • 877
  • 8
  • 14
1

This works for me:

__weak typeof(self) wself= self;
[self.contentCollectionView performBatchUpdates:^{
    [wself.contentCollectionView reloadData];
} completion:^(BOOL finished) {
    [wself pageViewCurrentIndexDidChanged:self.contentCollectionView];
}];
jAckOdE
  • 2,282
  • 7
  • 34
  • 64
1

I needed some action to be done on all of the visible cells when the collection view get loaded before it is visible to the user, I used:

public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if shouldPerformBatch {
        self.collectionView.performBatchUpdates(nil) { completed in
            self.modifyVisibleCells()
        }
    }
}

Pay attention that this will be called when scrolling through the collection view, so to prevent this overhead, I added:

private var souldPerformAction: Bool = true

and in the action itself:

private func modifyVisibleCells() {
    if self.shouldPerformAction {
        // perform action
        ...
        ...
    }
    self.shouldPerformAction = false
}

The action will still be performed multiple times, as the number of visible cells at the initial state. but on all of those calls, you will have the same number of visible cells (all of them). And the boolean flag will prevent it from running again after the user started interacting with the collection view.

gutte
  • 967
  • 1
  • 13
  • 26
1

Simply reload collectionView inside batch updates and then check in the completion block whether it is finished or not with the help of boolean "finish".

self.collectionView.performBatchUpdates({
        self.collectionView.reloadData()
    }) { (finish) in
        if finish{
            // Do your stuff here!
        }
    }
niku
  • 396
  • 2
  • 11
  • Not working for me, at least in iOS 14.1 simulator. The completion is never called. See this Radar: http://www.openradar.me/48941363 which seems to relate. Also, see this [SO](https://stackoverflow.com/a/47984323) that suggests it's not a good idea to be calling reloadData() from within performBatchUpdates. I had such high hopes for this elegant approach... – Smartcat Jan 04 '21 at 21:04
0

Def do this:

//Subclass UICollectionView
class MyCollectionView: UICollectionView {

    //Store a completion block as a property
    var completion: (() -> Void)?

    //Make a custom funciton to reload data with a completion handle
    func reloadData(completion: @escaping() -> Void) {
        //Set the completion handle to the stored property
        self.completion = completion
        //Call super
        super.reloadData()
    }

    //Override layoutSubviews
    override func layoutSubviews() {
        //Call super
        super.layoutSubviews()
        //Call the completion
        self.completion?()
        //Set the completion to nil so it is reset and doesn't keep gettign called
        self.completion = nil
    }

}

Then call like this inside your VC

let collection = MyCollectionView()

self.collection.reloadData(completion: {

})

Make sure you are using the subclass!!

Jon Vogel
  • 4,113
  • 1
  • 28
  • 34
0

This work for me:


- (void)viewDidLoad {
    [super viewDidLoad];

    int ScrollToIndex = 4;

    [self.UICollectionView performBatchUpdates:^{}
                                    completion:^(BOOL finished) {
                                             NSIndexPath *indexPath = [NSIndexPath indexPathForItem:ScrollToIndex inSection:0];
                                             [self.UICollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
                                  }];

}

chrisz
  • 45
  • 4
0

Below is the only approach that worked for me.

extension UICollectionView {
    func reloadData(_ completion: (() -> Void)? = nil) {
        reloadData()
        guard let completion = completion else { return }
        layoutIfNeeded()
        completion()
    }
}
Ashok
  • 5,085
  • 5
  • 45
  • 74
-1

This is how I solved problem with Swift 3.0:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    if !self.collectionView.visibleCells.isEmpty {
        // stuff
    }
}
landonandrey
  • 1,091
  • 1
  • 14
  • 25
  • This won't work. VisibleCells will have content as soon as the first cell is loaded... the problem is we want to know when the last cell has been loaded. – Travis M. Apr 06 '17 at 21:41
-9

Try this:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return _Items.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell;
    //Some cell stuff here...

    if(indexPath.row == _Items.count-1){
       //THIS IS THE LAST CELL, SO TABLE IS LOADED! DO STUFF!
    }

    return cell;
}
Rocker
  • 1
  • 2
    It is not correct, cellFor will get called only if that cell is going to be visible, in that case last visible item will not be as equal to total number of items... – Jirune Nov 08 '13 at 05:43
-10

You can do like this...

  - (void)reloadMyCollectionView{

       [myCollectionView reload];
       [self performSelector:@selector(myStuff) withObject:nil afterDelay:0.0];

   }

  - (void)myStuff{
     // Do your stuff here. This will method will get called once your collection view get loaded.

    }
Swapnil
  • 1,868
  • 2
  • 21
  • 47
  • Although this code may answer the question, providing additional context regarding _why_ and/or _how_ it answers the question would significantly improve its long-term value. Please [edit] your answer to add some explanation. – Toby Speight Jun 14 '16 at 15:29