UICollectionView旋转后屏幕尺寸获取错误 单元格宽度计算异常
问题根源
你的代码存在两处核心错误,直接导致旋转后单元格宽度计算异常:
- 布局刷新时机错误:在
viewWillTransition(to:with:)中直接调用invalidateLayout()时,控制器根视图、collectionView的bounds尚未更新到旋转后的目标尺寸,此时screenSize取到的view.bounds.size是旋转前的旧值。你虽然通过回调传入的size更新了isPortrait标记,但计算宽度的基准值还是旧尺寸,必然得到错误结果。 - 宽度计算基准不可靠:你以根视图bounds作为宽度计算基准,没有对齐collectionView实际的内容可用宽度,也没考虑系统自动调整的安全区域缩进、contentInset的实际值,一旦标记状态和视图尺寸更新不同步,计算结果就会和FlowLayout的实际排版宽度错位。
对应你描述的复现场景,错误触发流程是:
- 横屏启动时,根视图宽度为横屏值(以iPhone14为例约844pt),
isPortrait为false,计算得到的单元格宽度约270pt,一行放3个,符合预期。 - 横屏转竖屏时,
viewWillTransition里先把isPortrait改成true,但此时view.bounds还是横屏的844pt宽度,触发布局计算时走竖屏双列公式,算出的单元格宽度约410pt,远大于竖屏实际可用宽度,排版已经错位。 - 竖屏再转回横屏时,
viewWillTransition里把isPortrait改成false,但此时view.bounds还是竖屏的390pt宽度,触发布局计算时走横屏三列公式,算出的单元格宽度约120pt,844pt宽的横屏自然能放下4个单元格,就出现了你看到的异常。
修复方案
最小改动方案
保留现有逻辑,只修正布局刷新时机和宽度计算基准即可:
- 调整
viewWillTransition中的布局刷新逻辑,不要立刻调用invalidate,而是通过转场协调器在视图尺寸更新到目标值后再触发布局刷新:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) isPortrait = size.width < size.height coordinator.animate(alongsideTransition: { [weak self] _ in self?.collectionView.collectionViewLayout.invalidateLayout() }) }
- 修正
screenSize的取值逻辑,直接取collectionView的实际内容可用宽度,避免缩进计算偏差:
private var screenSize: CGSize { let horizontalInset = collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right let verticalInset = collectionView.adjustedContentInset.top + collectionView.adjustedContentInset.bottom return CGSize( width: collectionView.bounds.width - horizontalInset, height: collectionView.bounds.height - verticalInset ) }
注意:你代码里同时定义了Constants.collectionViewContentInsets和Constants.collectionViewInsets两个缩进常量,要保证计算时用的缩进值和实际赋值给collectionView的contentInset一致,否则还是会有宽度偏差。
更稳定的长期方案
去掉手动维护的isPortrait标记和手动旋转刷新逻辑,通过自定义FlowLayout实现bounds变化自动刷新,所有计算基于collectionView实时状态,从根源避免状态不同步问题:
- 自定义一个自动响应尺寸变化的FlowLayout:
class AutoRefreshFlowLayout: UICollectionViewFlowLayout { override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { // 只要bounds尺寸变化就自动触发布局刷新 return newBounds.size != collectionView?.bounds.size } }
- 初始化collectionView时替换为自定义layout:
let layout = AutoRefreshFlowLayout() layout.scrollDirection = .vertical layout.minimumLineSpacing = Constants.minimumLineSpacing layout.minimumInteritemSpacing = Constants.minimumInteritemSpacing // 其余collectionView配置不变
- 在
sizeForItemAt代理方法中直接基于collectionView实时bounds计算尺寸,不需要依赖外部标记:
func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath ) -> CGSize { let availableWidth = collectionView.bounds.width - collectionView.adjustedContentInset.left - collectionView.adjustedContentInset.right // 直接根据当前内容宽度判断横竖屏 let isPortrait = availableWidth < collectionView.bounds.height let cellWidth = isPortrait ? (availableWidth - Constants.minimumInteritemSpacing) / 2.0 : (availableWidth - Constants.minimumInteritemSpacing * 2.0) / 3.0 return CGSize(width: cellWidth, height: Constants.cellHeight) }
这个方案下不需要在viewWillTransition里写任何布局刷新逻辑,所有状态都来自collectionView的实时bounds,不会出现不同步问题,旋转时的布局动画也会和系统转场动画完全同步。
内容的提问来源于stack exchange,提问作者Kex
相关产品推荐
相关产品推荐

