Nested UICollectionViews, AutoLayout and rotation in iOS 8
Asked Answered
C

7

3

I started to use AutoLayout for a large project and was positively surprised about it. However, now I have to adjust the project to accommodate for rotation and size classes, and I have big troubles getting the views to behave correctly.

The base problem is that I have UICollectionViews with cells that again contain UICollectionViews. When the device is rotated, the base collection view adapts it's cells right, but all cells of the nested collection views are not adjusted to the new size automatically.

After investigation it comes down to:

  • When viewWillTransitionToSize is called, the base collection view is not rotated yet (as expected)
  • Sometime after that viewDidLayoutSubviews is called, the base collection view now has the rotated size - but unfortunately its cells not yet - they still have the width as BEFORE the rotation
  • Even in the coordinator animateAlongsideTransition ... completion:{} block in viewWillTransitionToSize the cells do not have their new width yet.

The above means that there is no place where I could possible tell the cells of the nested collection views to change their size, as I don't know the new widths yet; and the autolayout system does not automatically adjust them.

Does anyone else have the same problem or know a solution?

Comp answered 24/10, 2014 at 14:33 Comment(0)
C
9

Answering my own question here - I finally found the solution. For any others who are having troubles with this here is what I did:

The main part lies in putting invalidateLayout and reloadData calls in the right places:

  • Put an invalidateLayout on the base collectionView's layout in the willTransitionToSize function.
  • Put a reloadData on the base collectionView in the completion block of a coordinator animateAlongsideTransition: call in the willTransitionToSize function.
  • Make sure the autolayout constraints for any views in the cells are set right - I connected the cell view with autolayout constraints to the cell's content view in code.
  • In case you overwrote the prepareLayout function of the collection view layout, make sure the pre-calculated cell sizes use the right width, I used a wrong width here which lead to some additional problems.
Comp answered 28/10, 2014 at 8:40 Comment(1)
I was looking for this answer for over a year now. Rotation with a modal ViewController above just kept failing, now I understand what was missing. I implemented an example here: bitbucket.org/hansdesmedt/pattern-uicollectionviewlayout-iosGoldfilled
T
5

Thanks to TheEye. I wanted to add this as a comment to their response, but apparently I don't have enough Karma (yet), So consider this an addendum.

I was having the same problem, however the cells in my uicollectionview have an "expanded state" when selected. if the user rotates the device and we call [self.collectionview reloadData], any cells that are expanded will lose their state. You can optionally call performBatchUpdates and force the collection view to layout without losing the state of any cell

-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{
[self.collectionView.collectionViewLayout invalidateLayout];

[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {

} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    //[self.collectionView reloadData];//this works but reloading the view collapses any expanded cells.       
    [self.collectionView performBatchUpdates:nil completion:nil];       
}];  
}
Transmigrant answered 10/3, 2015 at 14:59 Comment(0)
A
1

TheKey's response is correct. However, instead of reloading the data, you may just call

UICollectionView.collectionViewLayout:finalizeCollectionViewUpdates()

Example:

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        self.collectionView.collectionViewLayout.invalidateLayout()
        self.collectionView.collectionViewLayout.finalizeCollectionViewUpdates()
    }
Ariel answered 27/8, 2015 at 11:26 Comment(0)
C
1

Wanted to offer an alternative solution. I didn't like the slight delay in calling either reloadData or performBatchUpdates in the coordinator's animation completion. Both also resulted in AutoLayout warnings.

Within viewWillTransitionToSize, calling invalidateLayout on all visible nested collection views and finally calling invalidateLayout on the outer/top level collection view.

My collection view cell which has a child collection view:

class NestableCollectionViewCell: UICollectionViewCell {
  @IBOutlet weak var collectionView: NestableCollectionView?
}

Extending UICollectionView to invalidate nested collection views:

class NestableCollectionView: UICollectionView {

  func invalidateNestedCollectionViews(){
    let visibleCellIndexPaths = self.indexPathsForVisibleItems()
    for cellIndex in visibleCellIndexPaths {
      if let nestedCell = self.cellForItemAtIndexPath(cellIndex) as? NestableCollectionViewCell {
        // invalidate any child collection views
        nestedCell.collectionView?.invalidateNestedCollectionViews()
        // finally, invalidate the cell's own collection view
        nestedCell.collectionView?.collectionViewLayout.invalidateLayout()
      }
    }
  }
}

And, invalidate on willTransitionToSize:

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
  super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

  self.collectionView.invalidateNestedCollectionViews()
  self.collectionView.collectionViewLayout.invalidateLayout()
}
Coarsegrained answered 23/12, 2015 at 17:26 Comment(0)
P
0

I too had the same problem, with the other answers I found that the layout was getting letterboxed. I got closer to what I wanted with this:

 override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: nil, completion:  { context in

            self.collectionView?.collectionViewLayout.invalidateLayout()
            self.collectionView?.collectionViewLayout.finalizeCollectionViewUpdates()

        })

    }
Powerless answered 10/6, 2018 at 5:26 Comment(0)
A
0

This has been an ongoing issue in our app but finally I achieved what I believe to be the best and easiest solution so far. Basically you'll need to remove the parent (base) collection view from the superview when device rotates and then add it again to the superview. If you do it properly the system will even animate it for you! Here's an example:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator);

    self.collectionView.removeFromSuperview();
    self.collectionView = self.getAFreshCollectionViewInstance();
    self.setupCollectionViewFrameUsingAutoLayout();
}

As for the data of the collection view, it works just fine by using the existing data source and this is really efficient when data is loaded from the web.

Attribution answered 17/6, 2018 at 4:50 Comment(0)
L
0

Just in case anyone is looking for a Swift 4 solution:

I had a very similar problem and I found the solution to be a combination of two answers above: use the coordinator animate function to call invalidateLayout() on the top collection view, then, in the completion hander, loop through each cell and call invalidateLayout() on each cell's collection view. I didn't have to maintain selections or expanded views so this worked without having to call reloadData().

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    // get the device orientation so we can change the number of rows & columns
    // in landscape and portrait modes
    let orientation = UIDevice.current.orientation

    if orientation.isLandscape {
        self.columns = 4
        self.rows = 3
    } else {
        self.columns = 3
        self.rows = 4
    }

    // invalidate the top collection view inside the coordinator animate function
    // then inside the animation completion handler, we invalidate each cell's colleciton view
    coordinator.animate(alongsideTransition: { (context) in

        self.collectionView.collectionViewLayout.invalidateLayout()

    }) { (context) in

        let cells = self.collectionView.visibleCells

        for cell in cells {
            guard let cell = cell as? MyCollectionViewCell else { continue }
            cell.invalidateLayout()
        }
    }
}
Lawn answered 16/8, 2018 at 18:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.