77

I used UICollectionView (flowlayout) to build a simple layout. the width for each cell is set to the width of screen using self.view.frame.width

but when I rotate the device, the cells don't get updated.

enter image description here

I have found a function, which is called upon orientation change :

override func willRotateToInterfaceOrientation(toInterfaceOrientation: 
  UIInterfaceOrientation, duration: NSTimeInterval) {
    //code
}

but I am unable to find a way to update the UICollectionView layout

The main code is here:

class ViewController: UIViewController , UICollectionViewDelegate , UICollectionViewDataSource , UICollectionViewDelegateFlowLayout{

    @IBOutlet weak var myCollection: UICollectionView!

    var numOfItemsInSecOne: Int!
    override func viewDidLoad() {
        super.viewDidLoad()

        numOfItemsInSecOne = 8
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {

        //print("orientation Changed")
    }

    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return numOfItemsInSecOne
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cellO", forIndexPath: indexPath)

        return cell
    }

    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{
    let itemSize = CGSize(width: self.view.frame.width, height: 100)
    return itemSize
    }}
Ajaxharg
  • 2,942
  • 1
  • 16
  • 19
Talha Ahmad Khan
  • 2,846
  • 3
  • 19
  • 35

14 Answers14

77

Add this function:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews() 
    myCollection.collectionViewLayout.invalidateLayout()
}

When you change the orientation, this function would be called.

appiconhero.co
  • 9,703
  • 11
  • 65
  • 102
  • thanks @khuong291 what if I have no object of my collectionview, and I have UICollectionViewController ? how will I suppose to call reloadData? – Talha Ahmad Khan Apr 27 '16 at 08:48
  • 2
    UICollectionViewController already has `collectionView` property, so you just call them as I said. Just edit them into `collectionView.reloadData()`. It will work. – appiconhero.co Apr 27 '16 at 08:52
  • 2
    So you need to reload the view, you can't just trigger a layout refresh? – Rivera Apr 27 '16 at 13:18
  • 2
    check @kelin's answer `.invalidateLayout()` makes a lot lot of sense. – thesummersign Feb 15 '17 at 11:05
  • 1
    `viewDidLayoutSubviews` makes my app crash when going back to portrait because old layout cells are too big. `viewWillLayoutSubviews ` works perfectly – jfgrang Sep 07 '18 at 12:43
  • @Khuong it's work for me but some time cells are too big while going from landscape view to portrait. why happen like this? – Nikunj Kumbhani Sep 20 '18 at 11:18
  • @NikunjKumbhani If so, I think you set layout wrong, please check it again. – appiconhero.co Sep 22 '18 at 07:37
  • 1
    Thanks for your code but it didn't work in my case too but solved it using notification. Here is the way how it work on my case https://stackoverflow.com/questions/18653171/issue-with-pagination-in-uicollectionview-when-orientation-changes – Yubraj Basyal Oct 04 '18 at 03:47
  • I had to useAs @jfgrang proposed, i had to use `viewWillLayoutSubviews` to really get the height and width of the cells re-calculated properly... I'd however did not crash in either case. – Hons Dec 03 '18 at 13:52
  • 4
    app hangs on app launch after using this. – rv7284 Jul 18 '19 at 05:29
  • 3
    This solution is not efficient as `viewDidLayoutSubviews()` is called many times when user scrolls the cells of the `UICollectionView`. – Imanou Petit Sep 04 '19 at 00:03
  • This should be viewWillLayout – Dan Rosenstark Nov 07 '19 at 12:56
  • I agree with @jfgrang: overriding `viewWillLayoutSubviews` is the only way I could get it to work (Xcode 11.4.1 + iOS 13.3). – titusmagnus Apr 30 '20 at 02:33
  • ```viewWillTransitionToSize``` worked, ```viewDidLayoutSubviews``` hanged the app. – Aidy May 16 '20 at 15:29
  • I currently use `viewWillTransitionToSize`, but, does not seem as bullet proof as `viewDidLayoutSubviews`. viewDidLayoutSubviews kills performance though. – zumzum Mar 26 '21 at 22:15
62

The better option is to call invalidateLayout() instead of reloadData() because it will not force recreation of the cells, so performance will be slightly better:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews() 
    myCollection.collectionViewLayout.invalidateLayout()
}
kelin
  • 9,553
  • 6
  • 63
  • 92
  • This is the correct way. This should be the accepted answer. – thesummersign Feb 15 '17 at 11:03
  • this is the correct way to do it.. not the reload data again – Abdul Waheed Apr 18 '17 at 05:23
  • 9
    that caused a crush for me. and an infinite loop recognized when i put a print statement in the viewDidLayoutSubviews function. – nyxee Jul 21 '17 at 07:27
  • 2
    @nyxee, try `viewWillLayoutSubviews` then. I bet, your Collection View is the view of the View Controller? If so, I recommend to wrap it into another view. – kelin Jul 21 '17 at 08:55
  • thanks.. I'll get back to that at some point. I've switched to tableViews for now. I was using the UICollectionViewController. – nyxee Jul 23 '17 at 05:06
  • @kelin May I suggest you change your answer to `viewWillLayoutSubviews`? As it is, the layout doesn't update correctly in current iOS versions. – Alexandre Cassagne Aug 11 '17 at 14:36
  • This might cause endless loops. @birdy's answer works better – arionik Apr 26 '18 at 21:26
  • 6
    The endless loop occurs if you use `UICollectionViewController`, because in this case the `collectionView` is also the view of the controller. So if you change `collectionView` layout `viewWillLayoutSubviews` and related methods will be called. – kelin Apr 26 '18 at 21:58
  • I tried every other highly voted answer in this thread and this is the only one **that worked**. Btw my collectionView is a subView of the viewController(`view.addSubview(myCollectionView)` -added in viewDidLoad). I put a print statement inside `viewWillLayoutSubviews` and it only printed on rotations. I didn't have any endless loops. – Lance Samaria Apr 07 '21 at 19:42
17

Also you can invalidate it in this way.

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

    [self.collectionView.collectionViewLayout invalidateLayout]; 
}
awuu
  • 206
  • 2
  • 5
15

The viewWillLayoutSubviews() did not work for me. Neither did viewDidLayoutSubviews(). Both made the app go into an infinite loop which I checked using a print command.

One of the ways that do work is

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// Reload here
}
The Doctor
  • 427
  • 1
  • 6
  • 15
14

To update UICollectionViewLayout, traitCollectionDidChange method might be used as well:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    guard previousTraitCollection != nil else { return }
    collectionView?.collectionViewLayout.invalidateLayout()
}
fredpi
  • 6,546
  • 3
  • 34
  • 57
birdy
  • 845
  • 13
  • 22
  • 2
    This worked best for me, the approach required is really dependent of your application of CollectionView, in my instance this was more compatible with the other behaviours of my custom FlowLayout than the other solutions because my items all have the same size that is dependent on the traitCollection – Rocket Garden Aug 19 '18 at 13:27
  • 3
    Brilliant - it also works when instantiating the UICollectionView within a custom UITableViewCell. – DrWhat Apr 29 '19 at 14:24
9

When UICollectionLayout detects a bounds change, it asks if it needs to reroute the Invalidate layout. You can rewrite the method directly.UICollectionLayout can call invalidateLayout method at the right time

class CollectionViewFlowLayout: UICollectionViewFlowLayout{
    
    /// The default implementation of this method returns false.
    /// Subclasses can override it and return an appropriate value
    /// based on whether changes in the bounds of the collection
    /// view require changes to the layout of cells and supplementary views.
    /// If the bounds of the collection view change and this method returns true,
    /// the collection view invalidates the layout by calling the invalidateLayout(with:) method.
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        
        return (self.collectionView?.bounds ?? newBounds) != newBounds
    }
}
ndg
  • 2,458
  • 2
  • 30
  • 51
zheng
  • 337
  • 2
  • 5
8

Please understand this

traitCollectionDidChange(previousTraitCollection :) won't be called on iPad when it is rotated, because size class is .regular in both portrait and landscape orientations. There's viewWillTransition(to:with:) that will be called whenever collection view size changes.

Also you shouldn't use UIScreen.mainScreen().bounds if your app supports multitasking as it might not occupy the whole screen, better use collectionView.frame.width for that.

Mehul
  • 2,500
  • 18
  • 30
4

Calling viewWillLayoutSubviews is not optimal. Try calling the invalidateLayout() method first.

If you experience the The behaviour of the UICollectionViewFlowLayout is not defined error, you need to verify if all the elements within your view's have changed its sizes, according to a new layout. (see the optional steps within the example code)

Here is the code, to get you started. Depending on the way your UI is created, you may have to experiment to find the right view to call the recalculate method, yet that should guide you towards your first steps.

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

    super.viewWillTransition(to: size, with: coordinator)

    /// (Optional) Additional step 1. Depending on your layout, you may have to manually indicate that the content size of a visible cells has changed
    /// Use that step if you experience the `the behavior of the UICollectionViewFlowLayout is not defined` errors.

    collectionView.visibleCells.forEach { cell in
        guard let cell = cell as? CustomCell else {
            print("`viewWillTransition` failed. Wrong cell type")
            return
        }

        cell.recalculateFrame(newSize: size)

    }

    /// (Optional) Additional step 2. Recalculate layout if you've explicitly set the estimatedCellSize and you'll notice that layout changes aren't automatically visible after the #3

    (collectionView.collectionViewLayout as? CustomLayout)?.recalculateLayout(size: size)


    /// Step 3 (or 1 if none of the above is applicable)

    coordinator.animate(alongsideTransition: { context in
        self.collectionView.collectionViewLayout.invalidateLayout()
    }) { _ in
        // code to execute when the transition's finished.
    }

}

/// Example implementations of the `recalculateFrame` and `recalculateLayout` methods:

    /// Within the `CustomCell` class:
    func recalculateFrame(newSize: CGSize) {
        self.frame = CGRect(x: self.bounds.origin.x,
                            y: self.bounds.origin.y,
                            width: newSize.width - 14.0,
                            height: self.frame.size.height)
    }

    /// Within the `CustomLayout` class:
    func recalculateLayout(size: CGSize? = nil) {
        estimatedItemSize = CGSize(width: size.width - 14.0, height: 100)
    }

    /// IMPORTANT: Within the `CustomLayout` class.
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {

        guard let collectionView = collectionView else {
            return super.shouldInvalidateLayout(forBoundsChange: newBounds)
        }

        if collectionView.bounds.width != newBounds.width || collectionView.bounds.height != newBounds.height {
            return true
        } else {
            return false
        }
    }
Michael
  • 1,097
  • 14
  • 31
4

My Code :

override func viewWillLayoutSubviews() {
   super.viewWillLayoutSubviews()
   self.collectionView.collectionViewLayout.invalidateLayout()
}

Will work correctly!!!

Duck
  • 32,792
  • 46
  • 221
  • 426
3

you can update your UICollectionView Layout by using

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
    if isLandscape {
        return CGSizeMake(yourLandscapeWidth, yourLandscapeHeight)
    }
    else {
        return CGSizeMake(yourNonLandscapeWidth, yourNonLandscapeHeight)
    }
}
Abhinandan Pratap
  • 2,048
  • 1
  • 14
  • 36
3

It works for me. And this is code in Objective-C:

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];
  [collectionView.collectionViewLayout invalidateLayout];
}
congpc87
  • 153
  • 1
  • 8
2

I was also having some problem but then it got solved using :

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionViewFlowLayoutSetup(with: view.bounds.size.width)
        collectionView?.collectionViewLayout.invalidateLayout()
        collectionViewFlowLayoutSetup(with: size.width)
    }

    fileprivate func collectionViewFlowLayoutSetup(with Width: CGFloat){

        if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(width: Width, height: 300)
        }

    }
2

I solve this by setting notification when screen orientation changes and reloading cell which set itemsize according to screen orientation and setting indexpath to previous cell. This does work with flowlayout too. Here is the code i wrote:

var cellWidthInLandscape: CGFloat = 0 {
    didSet {
        self.collectionView.reloadData()
    }
}

var lastIndex: Int = 0

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView.dataSource = self
    collectionView.delegate = self
    NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
    cellWidthInLandscape = UIScreen.main.bounds.size.width

}
deinit {
    NotificationCenter.default.removeObserver(self)
}
@objc func rotated() {

        // Setting new width on screen orientation change
        cellWidthInLandscape = UIScreen.main.bounds.size.width

       // Setting collectionView to previous indexpath
        collectionView.scrollToItem(at: IndexPath(item: lastIndex, section: 0), at: .right, animated: false)
}
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

   // Getting last contentOffset to calculate last index of collectionViewCell
    lastIndex = Int(scrollView.contentOffset.x / collectionView.bounds.width)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // Setting new width of collectionView Cell
        return CGSize(width: cellWidthInLandscape, height: collectionView.bounds.size.height)

}
Yubraj Basyal
  • 91
  • 1
  • 4
0

I solved the issue using below method

override func viewDidLayoutSubviews() {
        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            collectionView.collectionViewLayout.invalidateLayout()
            collectionView.collectionViewLayout = flowLayout
        }
    }
Community
  • 1
  • 1
CrazyPro007
  • 692
  • 4
  • 13