Swift: How to refresh UICollectionView layout after rotation of the device

IosSwiftUicollectionviewFlowlayout

Ios Problem Overview


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
    }}

Ios Solutions


Solution 1 - Ios

Add this function:

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

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

Solution 2 - Ios

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()
}

Solution 3 - Ios

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]; 
}

Solution 4 - Ios

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
}

Solution 5 - Ios

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()
}

Solution 6 - Ios

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
    }
}

Solution 7 - Ios

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.

Solution 8 - Ios

My Code :

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

Will work correctly!!!

Solution 9 - Ios

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
        }
    }

Solution 10 - Ios

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)
    }
}

Solution 11 - Ios

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

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];
  [collectionView.collectionViewLayout invalidateLayout];
}

Solution 12 - Ios

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)
        }
        
    }

Solution 13 - Ios

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)
    
}

Solution 14 - Ios

I solved the issue using below method

override func viewDidLayoutSubviews() {
        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            collectionView.collectionViewLayout.invalidateLayout()
            collectionView.collectionViewLayout = flowLayout
        }
    }

Solution 15 - Ios

In addition to what everybody else already mentioned, if you are creating the collection view yourself instead of subclassing UICollectionViewController, don't forget to set:

collectionView.translatesAutoresizingMaskIntoConstraints = false

When setting up the constraints.

Solution 16 - Ios

Try this:

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
  }

}

Documentation :

> 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTalha Ahmad KhanView Question on Stackoverflow
Solution 1 - IosKhuongView Answer on Stackoverflow
Solution 2 - IoskelinView Answer on Stackoverflow
Solution 3 - IosawuuView Answer on Stackoverflow
Solution 4 - IosThe DoctorView Answer on Stackoverflow
Solution 5 - IosbirdyView Answer on Stackoverflow
Solution 6 - IoszhengView Answer on Stackoverflow
Solution 7 - IosMehulView Answer on Stackoverflow
Solution 8 - IosКирилл СтаросельскийView Answer on Stackoverflow
Solution 9 - IosMichaelView Answer on Stackoverflow
Solution 10 - IosAbhinandan PratapView Answer on Stackoverflow
Solution 11 - Ioscongpc87View Answer on Stackoverflow
Solution 12 - Iosuser2698544View Answer on Stackoverflow
Solution 13 - IosYuvraj BashyalView Answer on Stackoverflow
Solution 14 - IosCrazyPro007View Answer on Stackoverflow
Solution 15 - Iosjulian avilaView Answer on Stackoverflow
Solution 16 - IosDhruvil PatelView Answer on Stackoverflow