Resize UICollectionViewCells On Rotation In Swift

This post extends the FullWidthCollectionViewCell implementation from Fixed Width, Dynamic Height UICollectionViewCells in Swift to add rotation support.

The previous post implemented a UICollectionView extension to expose a computed variable widestCellWidth. On rotation the bounds of the collection view change and therefore the widestCellWidth previously implemented for FullWidthCollectionViewCell changes as well. To resize the UICollectionViewCell on rotation a new approach is needed.

Subclass UICollectionViewFlowLayout To Refresh On Rotation

The best way to implement rotation support is to subclass UICollectionViewFlowLayout and override shouldInvalidateLayout. Doing so provides a deterministic method for changing cell sizes on rotation without causing autolayout constraint errors or undefined UICollectionView behavior.

Other implementations, like overriding traitCollectionDidChange and viewWillTransition, often cause autolayout constraint errors or other error messages during rotation. This happens because the incorrect bounds are used to compute new cell sizes after rotation. To properly support rotation, the new bounds after the rotation should be used to compute the estimatedItemSize.

UICollectionViewFlowLayout exposes exactly this api with the method shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool. Below is an implementation of a custom UICollectionViewFlowLayout that updates the estimateItemSize as needed on bounds changes when an iOS device rotates:

class AutoInvalidatingLayout: UICollectionViewFlowLayout {
    // Compute the width of a full width cell 
    // for a given bounds
    func widestCellWidth(bounds: CGRect) -> CGFloat {
        guard let collectionView = collectionView else { 
            return 0 
        }

        let insets = collectionView.contentInset        
        let width = bounds.width - insets.left - insets.right
        
        if width < 0 { return 0 }
        else { return width }
    }
    
    // Update the estimatedItemSize for a given bounds
    func updateEstimatedItemSize(bounds: CGRect) {
        estimatedItemSize = CGSize(
            width: widestCellWidth(bounds: bounds),
            // Make the height a reasonable estimate to
            // ensure the scroll bar remains smooth 
            height: 200
        )
    }

    // assign an initial estimatedItemSize by calling 
    // updateEstimatedItemSize. prepare() will be called
    // the first time a collectionView is assigned
    override func prepare() {
        super.prepare()

        let bounds = collectionView?.bounds ?? .zero
        updateEstimatedItemSize(bounds: bounds)
    }
    
    // If the current collectionView bounds.size does 
    // not match newBounds.size, update the 
    // estimatedItemSize via updateEstimatedItemSize 
    // and invalidate the layout
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        guard let collectionView = collectionView else { 
            return false 
        }
        
        let oldSize = collectionView.bounds.size
        guard oldSize != newBounds.size else { return false }
        
        updateEstimatedItemSize(bounds: newBounds)
        return true
    }
}

Assign The Custom UICollectionViewFlowLayout

The last step is to assign an instance of AutoInvalidatingLayout as the collectionViewLayout on a collection view:

// The custom UICollectionViewFlowLayout will automatically
// refresh the layout on rotation
collectionView.collectionViewLayout = AutoInvalidatingLayout()

Updating UICollectionView Layout After Rotating The Screen

Combined with the FullWidthCollectionViewCell implementation from Fixed Width, Dynamic Height UICollectionViewCells in Swift, the new AutoInvalidatingLayout smoothly refreshes the collection view layout and cell size after rotation:

Portrait UICollectionViewCell layouts on an iPhone 8 simulator

Landscape UICollectionViewCell layouts on an iPhone 8 simulator