11

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.

Jordan H
  • 52,571
  • 37
  • 201
  • 351
  • Did you look into http://stackoverflow.com/questions/29023473/uicollectionview-invalidate-layout-on-bounds-changes? – Ozgur Vatansever May 21 '16 at 20:55
  • @ozgur I already tried what is in that answer yes, as explained above – Jordan H May 21 '16 at 20:57
  • Are you sure you are not updating anything in `viewWillAppear()`? In the described scenario (you change orientation while the modal controller is over) when the modal view controller is being dismissed you may get the frame with the *previous* orientation in `viewWillAppear()`, and the correct frame in `viewDidAppear()`. So what usually works for me is only updating the layout in the `viewWillTransitionToSize`. – Alex Staravoitau May 21 '16 at 21:00
  • @AlexStaravoitau Nope nothing in `viewWillAppear`. `viewWillTransitionToSize` is the only place I invalidate the layout. – Jordan H May 21 '16 at 21:02
  • Invalidating the layout again in `viewWillAppear` does resolve the issue with the modal view controller behavior. Not a very clean solution, perhaps there are other situations where it wouldn't work as expected too? – Jordan H May 21 '16 at 21:04
  • I would check the case when you present a modal view controller with current context (e.g. change its `presentationStyle`), for instance if the modal view controller is semi-transparent. In this case you might not get the `viewWillAppear()` callback called in the presenting view controller, since it technically never disappears. – Alex Staravoitau May 21 '16 at 21:13
  • I'll look into that. Right now it's form sheet, which adapts to over full screen for iPhone. – Jordan H May 21 '16 at 21:19
  • Interesting. Over current context works as expected without invalidating in `viewWillAppear`, but current context doesn't - not sized properly – Jordan H May 21 '16 at 21:23
  • You could also try `viewDidLayoutSubviews()` I guess. This should be called whenever the layout changes and the views have their final frames. – Alex Staravoitau May 21 '16 at 22:07

1 Answers1

30

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.

Jordan H
  • 52,571
  • 37
  • 201
  • 351