Proper way to invalidate collection view layout upon size change

Jordan H picture Jordan H · May 21, 2016 · Viewed 9.4k times · Source

I'm implementing a collection view whose items are sized based on the bounds of the collection view. Therefore when the size changes, due to rotating the device for example, I need to invalidate the layout so that the cells are resized to consider the new collection view bounds. I have done this via the viewWillTransitionToSize API.

This works well until the user presents a modal view controller over the view controller that contains the collection view, rotates the device, then dismisses it. When that occurs the item size hasn't updated to the appropriate size. viewWillTransitionToSize is called and the layout is invalidated as expected, but the collection view's bounds is still the old value. For example when rotating from portrait to landscape the collection view bounds value still has its height greater than the width. I'm not sure why that's the case, but I'm wondering if this is the best way to invalidate upon size change? Is there a better way to do it?

I have also tried subclassing UICollectionViewFlowLayout and overriding shouldInvalidateLayoutForBoundsChange to return YES, but for some reason this doesn't work even rotating without a modal presentation. It doesn't use the proper collection view bounds either.

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(nonnull id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [self.collectionView.collectionViewLayout invalidateLayout];

    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  __nonnull context) {
        [self.collectionView.collectionViewLayout invalidateLayout];
    } completion:^(id<UIViewControllerTransitionCoordinatorContext>  __nonnull context) {
        //finished
    }];
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //collectionView.bounds.size is not always correct here after invalidating layout as explained above
}

I've also tried invaliding it in the completion block but it still doesn't use the proper collection view bounds.

If I invalidate the layout again in viewWillAppear, this does use the proper collection view bounds which resolves the issue with rotating with the modally presented view controller. But this shouldn't be necessary, perhaps there are other situations where this won't be sized properly.

Answer

Jordan H picture Jordan H · May 22, 2016

I know what the problem is. When you call invalidateLayout inside animateAlongsideTransition (either in the animation block or the completion block), it doesn't actually recalculate the layout if there is a modal view controller presented over full screen (but it will if it's over current context). But it will invalidate it if you invalidate it outside of the animation block like I was doing. At that time however the collection view hasn't laid out for the new size, which explains why I was seeing the old value for its bounds. The reason for this behavior is invalidateLayout does not immediately cause the layout to be recalculated - it is scheduled to occur during the next layout pass. This layout pass does not occur when there's a modally presented view controller over full screen. To force a layout pass, simply call [self.collectionView layoutIfNeeded]; immediately after [self.collectionView.collectionViewLayout invalidateLayout];, still within the animateAlongsideTransition block. This will cause the layout to be recalculated as expected.