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

UICollectionViewLayout子类中合法获取单元格尺寸计算布局的方法

自定义UICollectionViewLayout获取自适应单元格尺寸的正确方式

问题描述

我正在编写自定义UICollectionViewLayout子类,想要完全控制单元格的布局排列。原本的思路是先让单元格自适应内容得到初始尺寸,再基于这个尺寸调整框架。于是在prepare()方法里通过集合视图的dataSource获取所有单元格,用systemLayoutSizeFitting(_:)计算尺寸:

let allCells = allIndedxPaths.compactMap {     
    collectionView.dataSource?.collectionView(collectionView, cellForItemAt: $0) 
}

let allItemSizes = allCells.map { cell in
    cell.systemLayoutSizeFitting(.init(width: contentSize.width, height: UIView.noIntrinsicMetric))
}

视觉效果没问题,但每次布局控制台都会输出重入警告:

[CollectionView] An attempt to prepare a layout while a prepareLayout call was already in progress (i.e. reentrant call) has been ignored. Please file a bug.

显然布局准备阶段不能直接调用数据源的cellForItemAt:方法,想知道怎么合法获取布局所需的数据源信息,尤其是准确的单元格尺寸。

补充背景:

  • Apple官方的UICollectionViewFlowLayout支持自动单元格尺寸(estimatedItemSize),说明存在合理实现方式。
  • Apple官方文档提到,布局对象在少数情况下可以依赖数据源的信息(比如地图布局获取坐标),说明有合法查询数据源的途径。

解决方案

方案1:让数据源提供尺寸计算所需的原始数据

这是最推荐的方式,避免直接操作Cell实例。让数据源暴露获取单元格内容的接口,布局类通过这些内容自行计算尺寸,或者让数据源直接提供预计算好的尺寸。

比如定义一个协议:

protocol TagListLayoutDataSource: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, contentForItemAt indexPath: IndexPath) -> String
    func collectionView(_ collectionView: UICollectionView, fontForItemAt indexPath: IndexPath) -> UIFont
}

然后在布局类里通过这个协议获取内容,计算尺寸:

override func prepare() {
    guard let collectionView = collectionView,
          let dataSource = collectionView.dataSource as? TagListLayoutDataSource else {
        return
    }

    contentSize = CGSize(width: collectionView.bounds.width, height: 0)
    let itemCount = collectionView.numberOfItems(inSection: 0)
    var allItemSizes: [CGSize] = []

    for item in 0..<itemCount {
        let indexPath = IndexPath(item: item, section: 0)
        let text = dataSource.collectionView(collectionView, contentForItemAt: indexPath)
        let font = dataSource.collectionView(collectionView, fontForItemAt: indexPath)
        
        // 计算文本尺寸
        let size = text.size(withAttributes: [.font: font])
        // 加上Cell内部的padding
        let cellSize = CGSize(width: size.width + 16, height: size.height + 8)
        allItemSizes.append(cellSize)
    }

    // 后续的frame计算逻辑不变...
}

方案2:使用估算尺寸 + 动态更新布局

参考UICollectionViewFlowLayout的estimatedItemSize逻辑,先设置一个估算尺寸,在Cell显示后获取真实尺寸,再更新布局。

  1. 先在布局类里添加估算尺寸属性:
var estimatedItemSize: CGSize = CGSize(width: 100, height: 30)
  1. 在prepare()里先用估算尺寸计算初始布局:
override func prepare() {
    guard let collectionView = collectionView else { return }
    contentSize = CGSize(width: collectionView.bounds.width, height: 0)
    let itemCount = collectionView.numberOfItems(inSection: 0)
    let allItemSizes = Array(repeating: estimatedItemSize, count: itemCount)
    frames = computeFrames(for: allItemSizes)
    // 缓存属性...
    contentSize.height = frames.map(\.value.maxY).max() ?? 0
}
  1. 监听Cell的显示,获取真实尺寸后更新布局:
    可以在Cell的layoutSubviews里触发布局更新:
class TagCell: UICollectionViewCell {
    override func layoutSubviews() {
        super.layoutSubviews()
        guard let collectionView = superview as? UICollectionView,
              let layout = collectionView.collectionViewLayout as? SelfSizingTagListLayout,
              let indexPath = collectionView.indexPath(for: self) else {
            return
        }
        let realSize = systemLayoutSizeFitting(CGSize(width: collectionView.bounds.width, height: UIView.noIntrinsicMetric))
        // 如果真实尺寸和估算的不一样,触发布局更新
        if layout.cachedAttributes[indexPath]?.size != realSize {
            collectionView.performBatchUpdates(nil)
        }
    }
}

这种方式的好处是不需要提前计算所有Cell的尺寸,只需要处理可见的Cell,性能更好。

方案3:手动实例化Cell计算尺寸

如果必须通过Cell的systemLayoutSizeFitting计算尺寸,不要调用数据源的cellForItemAt:,而是手动实例化Cell并配置内容,再计算尺寸:

override func prepare() {
    guard let collectionView = collectionView else { return }
    contentSize = CGSize(width: collectionView.bounds.width, height: 0)
    let itemCount = collectionView.numberOfItems(inSection: 0)
    var allItemSizes: [CGSize] = []
    
    for item in 0..<itemCount {
        let indexPath = IndexPath(item: item, section: 0)
        // 手动实例化Cell
        let cell = TagCell()
        // 从数据源获取内容并配置Cell
        if let dataSource = collectionView.dataSource as? TagListLayoutDataSource {
            dataSource.collectionView(collectionView, configureCell: cell, at: indexPath)
        }
        // 计算尺寸
        let size = cell.systemLayoutSizeFitting(CGSize(width: contentSize.width, height: UIView.noIntrinsicMetric))
        allItemSizes.append(size)
    }
    
    // 后续的frame计算逻辑不变...
}

这里需要给数据源添加一个配置Cell的协议方法:

protocol TagListLayoutDataSource: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, configureCell cell: UICollectionViewCell, at indexPath: IndexPath)
}

这种方式避免了重入问题,但需要确保手动实例化的Cell和数据源返回的Cell配置逻辑一致,同时如果Cell数量多的话,性能可能不如方案1。

总结

优先选择方案1,让数据源提供原始内容或预计算尺寸,这是最符合CollectionView设计模式的方式;如果无法修改数据源,考虑方案3手动实例化Cell计算尺寸;如果追求性能,尤其是Cell数量多的场景,推荐方案2的估算尺寸+动态更新。

内容的提问来源于stack exchange,提问作者Mischa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 11:55:46