4

I have a UICollectionViewController that has a bunch of cells, and each cell has the screen size.

Everything works as expected if I launch the app either in portrait or in landscape.

The problem is when I change the screen orientation, and I call collectionViewLayout.invalidateLayout() the cells size stays as it was before, without resizing them.

I don't know why this is happening.

Here's some code:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    print("size for item")
    return CGSize(width: view.frame.width, height: view.frame.height)
}


override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    coordinator.animate(alongsideTransition: { (_) in
        self.collectionViewLayout.invalidateLayout() // layout update
    }, completion: nil)
}

On the first load, I get the prints from the size set, but after changing orientation, it doesn't call it, so when I change orientation the cells size doesn't fit the screen as it fits on launch.

Any ideia what maybe causing this or what am I doing wrong here?

Thank you

EDIT

Somehow I tried to insert a print statement inside the willTransition function and I noticed it doesn't fire on screen rotation. I think the problem is there instead of the invaldateLayout() since it never fires.

How can this override function not be automatically called?

Ivan Cantarino
  • 3,058
  • 4
  • 34
  • 73

3 Answers3

10

There is a dedicated flag for that on UICollectionViewFlowLayoutInvalidationContext called invalidateFlowLayoutDelegateMetrics. The documentation reads:

The default value of this property is false. Set this property to true if you are invalidating the layout because of changes to the size of any items. When this property is set to true, the flow layout object recomputes the size of its items and views, querying the delegate object as needed for that information.

Raise the flag in flow layout subclass.

class CollectionViewFlowLayout: UICollectionViewFlowLayout {
        
    override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
        let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
        if let collectionView = collectionView {
            context.invalidateFlowLayoutDelegateMetrics = collectionView.bounds.size != newBounds.size
        }
        return context
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
Wojciech Nagrodzki
  • 2,788
  • 15
  • 13
  • 1
    Great! Few people mentioned this across the web. I used it in combination with `shouldInvalidateLayout(forBoundsChange newBounds: CGRect)` – Husam Jan 10 '21 at 22:56
4

What I can recommend is overriding the layout class and implementing shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool

class CollectionViewLayoutSelfSizingCells : UICollectionViewFlowLayout 
{    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        if let collectionView = self.collectionView {
            return collectionView.frame.size != newBounds.size
        }

        return false
    }
}

This is nicest solution I found and it's working across whole project. To be honest I was quite surprised that is not default on UICollectionViewFlowLayout

Grzegorz Krukowski
  • 18,081
  • 5
  • 50
  • 71
  • Cool stuff, can also do via extension `extension UICollectionViewFlowLayout { open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {` – Alexandre G Oct 08 '18 at 07:47
  • 3
    @AlexandreG I'm pretty sure you don't want to do that via extension as it will change behaviour of all layouts you have in the application - and that's not something you want. – Grzegorz Krukowski Oct 09 '18 at 19:27
2

You should implement the func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) method. This is called whenever the view controller's size will change, such as when rotating to/from portrait and landscape:

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

    coordinator.animate(alongsideTransition: { (_) in
        self.collectionViewLayout.invalidateLayout() // layout update
    }, completion: nil)
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • OMG, you're right. I have confused the delegate method. I'm using `willTransition` when I should be using `viewWillTransition` as you've mentioned. Thank you :D – Ivan Cantarino Oct 31 '17 at 22:47