61

Does anyone know why contentSize is not updated immediately after reloadData is called on UICollectionView?

If you need to know the contentSize the best work around I've found is the following:

[_collectionView reloadData];

double delayInSeconds = 0.0001;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(DISPATCH_TIME_NOW, dispatch_get_main_queue(), ^(void)
    {
        // TODO: Whatever it is you want to do now that you know the contentSize.
    });

Obviously this is a fairly brittle hack that makes assumptions on Apple's implementation but so far it has proven to work quite reliably.

Does anyone have any other workarounds or knowledge on why this happens? I'm debating submitting a radar because this I can't understand why they cannot calculate the contentSize in the same run loop. That is how UITableView has worked for its entire implementation.

EDIT: This question use to reference the setContentOffset method inside the block because I want to scroll the collection view in my app. I've removed the method call because peoples' answers focused on why wasn't I using scrollToItemAtIndexPath inside of why contentSize is not being updated.

Reid Main
  • 3,394
  • 3
  • 25
  • 42
  • 1
    I think the more appropriate way would be to use scrollToItemAtIndexPath:atScrollPosition:animated:. As for why the content size isn't updated, I can only assume its an optimization to prevent either calling potentially numerous and expensive dynamic size calculations or returning inaccurate information and by extension doing unnecessary processing and work. – Matt Sep 30 '13 at 20:48
  • In the actual app I do use that. For simplicities sake I chose to mention setContentOffset. That is a good point about reloadData being called multiple times could trigger expensive calculations. Too bad there isn't a reloadData with completion handler method. – Reid Main Sep 30 '13 at 20:50
  • 1
    Call `layoutIfNeeded` on `UICollectionView` before accessing its `contentSize` – onmyway133 Mar 05 '20 at 11:07

6 Answers6

85

To get the content size after reload, try to call collectionViewContentSize of the layout object. It works for me.

Clement T
  • 886
  • 7
  • 7
  • 3
    I was skeptical at first but hey it worked for me too :) – Zhang Jun 09 '14 at 04:58
  • It does look like calls to reloadData don't immediately trigger the collection view to change its render properties but like Clement suggested you can go directly to the layout object to get this information immediately. – Reid Main May 23 '15 at 17:42
  • 4
    SWIFT 3: let contentSize = self.myCollectionView.collectionViewLayout.collectionViewContentSize – drpawelo Oct 23 '17 at 14:35
  • This solved my issue also :) was calling perform batch updates to get content height and got crashes but my main issue was the content height was not there yet. But my layout had the proper height. – Andrew Edwards Dec 06 '18 at 18:16
14

This worked for me:

[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView.collectionViewLayout prepareLayout];
almas
  • 7,090
  • 7
  • 33
  • 49
8

Edit: I've just tested this and indeed, when the data changes, my original solution will crash with the following:

"Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (7) must be equal to the number of items contained in that section before the update (100), plus or minus the number of items inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out)."

The right way to handle this is to calculate the insertions, deletions, and moves whenever the data source changes and to use performBatchUpdates around them when it does. For example, if two items are added to the end of an array which is the data source, this would be the code:

NSArray *indexPaths = @[indexPath1, indexPath2];
[self.collectionView performBatchUpdates:^() 
{
    [self.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
    // TODO: Whatever it is you want to do now that you know the contentSize.
}];

Below is the edited solution of my original answer provided by Kernix which I don't think is guaranteed to work.

Try performBatchUpdates:completion: on UICollectionView. You should have access to the updated properties in the completion block. It would look like this:

[self.collectionView reloadData];
[self.collectionView performBatchUpdates:^() 
{

} completion:^(BOOL finished) {
    // TODO: Whatever it is you want to do now that you know the contentSize.
}];
brodney
  • 1,176
  • 2
  • 14
  • 29
  • 1
    That does look like a solid solution. The changes will animate which isn't what I want but I'm sure there has to be a way to kill the animations. – Reid Main Oct 15 '13 at 15:17
  • You can override initial layout attributes in the layout subclass using initialLayoutAttributesForAppearingItemAtIndexPath:. Set the frame there to be the frame of the cells that change. – brodney Oct 15 '13 at 16:57
  • 1
    I'm pretty sure this will crash, you should put the reloadData out of the performBatch block. – Aurelien Porte Jan 29 '14 at 15:20
2

After calling

[self.collectionView reloadData];

use this:

[self.collectionView setNeedsLayout];
[self.collectionView layoutIfNeeded];

then you can get the real contentSize

Ben Shabat
  • 388
  • 4
  • 17
-3

Call prepareLayout first, and then you will get correct contentSize:

[self.collectionView.collectionViewLayout prepareLayout];
Robert Mao
  • 1,911
  • 22
  • 24
  • 2
    Incorrect. The default implementation does nothing, as stated by Apple: https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UICollectionViewLayout_class/index.html#//apple_ref/occ/instm/UICollectionViewLayout/prepareLayout – Can Poyrazoğlu Jun 08 '15 at 19:59
  • 1
    agreeing with @CanPoyrazoğlu – Max MacLeod Aug 17 '15 at 09:22
  • This is interesting - I have a case in which calling invalidateLayout followed by reloadData will not trigger a sizeForItem: call. However, calling prepareLayout directly between invalidate and reload causes the sizes to be recalculated during reload as I would expect... The docs do say the implementation is empty, but I'm not sure I totally believe this... – tyler Sep 14 '16 at 16:24
  • 1
    @CanPoyrazoğlu the docs say it the method definition is empty for `UICollectionViewLayout`. In many cases you will be using the default subclass, `UICollectionViewFlowLayout`, which it seems does not have an empty implementation, and why it is working for most. – Jordan Smith Apr 27 '17 at 23:52
-3

In my case, I had a custom layout class that inherits UICollectionViewFlowLayout and set it only in the Interface Builder. I accidentally removed the class from the project but Xcode didn't give me any error, and the contentSize of the collection view returned always zero.

I put back the class and it started working again.

da1
  • 487
  • 5
  • 10