如何在CollectionView组合布局中实现表格列宽统一与双向滚动?
基于UICollectionViewCompositionalLayout的双向滚动表格问题解决
问题背景
需要实现支持双向滚动的N行N列表格,基于UICollectionViewCompositionalLayout搭建,要求:
- 单元格最大宽度130,高度随内容自适应
- 同行单元格高度统一,同列单元格宽度统一
- 单元格仅含标签,行数无限制
- 单个视图可容纳多个表格
当前采用自定义CustomLayout,通过重写layoutAttributesForElements计算行列最大宽高并设置,但出现两个问题:
- 初始加载时同列单元格宽度不生效,横向滚动到末尾后才恢复正常
- 滚动过程中部分单元格会隐藏
问题根源
- 初始列宽失效:
UICollectionViewCompositionalLayout默认仅提前布局可见区域内的元素,初始阶段未加载全所有列的单元格,导致无法计算完整的列最大宽度,只有滚动到末尾加载完所有列后,才能拿到全量宽度数据进行修正。 - 滚动时单元格隐藏:自定义布局要么未正确实现
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
相关产品推荐
相关产品推荐

