You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UICollectionView旋转后屏幕尺寸获取错误 单元格宽度计算异常

问题根源

你的代码存在两处核心错误,直接导致旋转后单元格宽度计算异常:

  • 布局刷新时机错误:在viewWillTransition(to:with:)中直接调用invalidateLayout()时,控制器根视图、collectionView的bounds尚未更新到旋转后的目标尺寸,此时screenSize取到的view.bounds.size是旋转前的旧值。你虽然通过回调传入的size更新了isPortrait标记,但计算宽度的基准值还是旧尺寸,必然得到错误结果。
  • 宽度计算基准不可靠:你以根视图bounds作为宽度计算基准,没有对齐collectionView实际的内容可用宽度,也没考虑系统自动调整的安全区域缩进、contentInset的实际值,一旦标记状态和视图尺寸更新不同步,计算结果就会和FlowLayout的实际排版宽度错位。

对应你描述的复现场景,错误触发流程是:

  1. 横屏启动时,根视图宽度为横屏值(以iPhone14为例约844pt),isPortrait为false,计算得到的单元格宽度约270pt,一行放3个,符合预期。
  2. 横屏转竖屏时,viewWillTransition里先把isPortrait改成true,但此时view.bounds还是横屏的844pt宽度,触发布局计算时走竖屏双列公式,算出的单元格宽度约410pt,远大于竖屏实际可用宽度,排版已经错位。
  3. 竖屏再转回横屏时,viewWillTransition里把isPortrait改成false,但此时view.bounds还是竖屏的390pt宽度,触发布局计算时走横屏三列公式,算出的单元格宽度约120pt,844pt宽的横屏自然能放下4个单元格,就出现了你看到的异常。
修复方案

最小改动方案

保留现有逻辑,只修正布局刷新时机和宽度计算基准即可:

  1. 调整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()
    })
}
  1. 修正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实时状态,从根源避免状态不同步问题:

  1. 自定义一个自动响应尺寸变化的FlowLayout:
class AutoRefreshFlowLayout: UICollectionViewFlowLayout {
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        // 只要bounds尺寸变化就自动触发布局刷新
        return newBounds.size != collectionView?.bounds.size
    }
}
  1. 初始化collectionView时替换为自定义layout:
let layout = AutoRefreshFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = Constants.minimumLineSpacing
layout.minimumInteritemSpacing = Constants.minimumInteritemSpacing
// 其余collectionView配置不变
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 12:09:27