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
- Assign The Custom UICollectionViewFlowLayout
- Updating UICollectionView Layout After Rotating The Screen
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: