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

如何在CollectionView组合布局中实现表格列宽统一与双向滚动?

基于UICollectionViewCompositionalLayout的双向滚动表格问题解决

问题背景

需要实现支持双向滚动的N行N列表格,基于UICollectionViewCompositionalLayout搭建,要求:

  • 单元格最大宽度130,高度随内容自适应
  • 同行单元格高度统一,同列单元格宽度统一
  • 单元格仅含标签,行数无限制
  • 单个视图可容纳多个表格

当前采用自定义CustomLayout,通过重写layoutAttributesForElements计算行列最大宽高并设置,但出现两个问题:

  1. 初始加载时同列单元格宽度不生效,横向滚动到末尾后才恢复正常
  2. 滚动过程中部分单元格会隐藏

问题根源

  1. 初始列宽失效:UICollectionViewCompositionalLayout默认仅提前布局可见区域内的元素,初始阶段未加载全所有列的单元格,导致无法计算完整的列最大宽度,只有滚动到末尾加载完所有列后,才能拿到全量宽度数据进行修正。
  2. 滚动时单元格隐藏:自定义布局要么未正确实现layoutAttributesForItem(at:)的返回逻辑,要么更新布局属性时未考虑滚动偏移导致的布局失效,也可能是单元格frame计算错误,超出了collectionView可视区域或被其他元素覆盖。

解决方案

1. 提前预计算行列尺寸(解决初始列宽问题)

不要依赖可见单元格计算尺寸,直接遍历数据源提前算出每列的最大宽度、每行的最大高度并缓存,供布局使用:

class CustomLayout: UICollectionViewCompositionalLayout {
    private var cachedColumnWidths: [CGFloat] = []
    private var cachedRowHeights: [CGFloat] = []
    private let tableDataSource: [[String]]
    private let cellFont = UIFont.systemFont(ofSize: 14)
    
    init(dataSource: [[String]]) {
        self.tableDataSource = dataSource
        super.init(sectionProvider: { _, _ in
            // 基础section配置,后续会重写布局属性
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            return NSCollectionLayoutSection(group: group)
        })
        precomputeSizes()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 预计算行列尺寸
    private func precomputeSizes() {
        let rowCount = tableDataSource.count
        let columnCount = tableDataSource.first?.count ?? 0
        
        cachedColumnWidths = Array(repeating: 0, count: columnCount)
        cachedRowHeights = Array(repeating: 0, count: rowCount)
        
        for rowIndex in 0..<rowCount {
            var currentRowMaxHeight: CGFloat = 0
            for columnIndex in 0..<columnCount {
                let content = tableDataSource[rowIndex][columnIndex]
                // 计算内容所需宽度(不超过130)
                let contentWidth = content.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 1000),
                                                       options: .usesLineFragmentOrigin,
                                                       attributes: [.font: cellFont],
                                                       context: nil).width
                let targetWidth = min(contentWidth, 130)
                cachedColumnWidths[columnIndex] = max(cachedColumnWidths[columnIndex], targetWidth)
                
                // 基于列宽计算内容高度
                let contentHeight = content.boundingRect(with: CGSize(width: targetWidth, height: CGFloat.greatestFiniteMagnitude),
                                                        options: .usesLineFragmentOrigin,
                                                        attributes: [.font: cellFont],
                                                        context: nil).height
                currentRowMaxHeight = max(currentRowMaxHeight, contentHeight)
            }
            cachedRowHeights[rowIndex] = currentRowMaxHeight
        }
    }
}

2. 正确实现布局属性方法(解决滚动隐藏问题)

重写布局核心方法,用预计算的尺寸生成正确的单元格frame,同时确保collectionView的滚动范围正确:

extension CustomLayout {
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let baseAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
              indexPath.row < tableDataSource.count,
              indexPath.item < (tableDataSource[indexPath.row].count) else {
            return nil
        }
        
        let row = indexPath.row
        let column = indexPath.item
        
        // 计算X偏移:累加前面所有列的宽度
        var xOffset: CGFloat = 0
        for col in 0..<column {
            xOffset += cachedColumnWidths[col]
        }
        
        // 计算Y偏移:累加前面所有行的高度
        var yOffset: CGFloat = 0
        for r in 0..<row {
            yOffset += cachedRowHeights[r]
        }
        
        baseAttributes.frame = CGRect(x: xOffset,
                                      y: yOffset,
                                      width: cachedColumnWidths[column],
                                      height: cachedRowHeights[row])
        return baseAttributes
    }
    
    override var collectionViewContentSize: CGSize {
        let totalWidth = cachedColumnWidths.reduce(0, +)
        let totalHeight = cachedRowHeights.reduce(0, +)
        return CGSize(width: totalWidth, height: totalHeight)
    }
    
    override func invalidateLayout() {
        precomputeSizes()
        super.invalidateLayout()
    }
}

3. 多表格场景适配

如果单个视图包含多个表格,每个CustomLayout实例需单独维护自己的数据源和缓存数据,避免不同表格的尺寸互相干扰:

// 使用示例:给每个表格传入独立的数据源
let table1DataSource = [["单元格1-1", "长内容单元格1-2"], ["单元格2-1", "单元格2-2"]]
let layout1 = CustomLayout(dataSource: table1DataSource)
let collectionView1 = UICollectionView(frame: CGRect(x: 0, y: 0, width: 300, height: 200), collectionViewLayout: layout1)

let table2DataSource = [["表格2单元格1-1"], ["表格2单元格2-1"], ["表格2单元格3-1"]]
let layout2 = CustomLayout(dataSource: table2DataSource)
let collectionView2 = UICollectionView(frame: CGRect(x: 0, y: 220, width: 300, height: 200), collectionViewLayout: layout2)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 03:15:00