使用UICollectionViewCompositionalLayout如何为特定数据源项设自定义尺寸?
UICollectionView组合布局:实现多数项多列、特殊项全屏宽度的方案
常规多列组合布局示例
下面是典型的3列UICollectionView组合布局实现代码:
let layout = UICollectionViewCompositionalLayout { (_, _) -> NSCollectionLayoutSection? in let layoutSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1/CGFloat(3)), heightDimension: .absolute(rowHeight) ) let item = NSCollectionLayoutItem(layoutSize: layoutSize) let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(rowHeight) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) return NSCollectionLayoutSection(group: group) }
该布局会将列表项按3列均匀排列。但实际开发中,常遇到需求:同一分区内,大部分项保持多列布局,少数特殊项(根据数据源标记)需要占据全屏宽度。
尝试过的方案及卡点
我试过三种思路,均遇到无法解决的问题:
方案A:使用
sizeForItemAt代理方法
无法在代理中返回组合布局原本的默认尺寸,代码示例:func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { if /* 判断是否为特殊项 */ { return /* 特殊项尺寸 */ } else { return /* 不知道怎么返回组合布局的默认尺寸 */ } }方案B:在布局中根据项设置宽度
组合布局的创建闭包内无法获取具体项的indexPath,没法针对性调整尺寸:let layoutSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1/ item == 666 ? 1 : 3 ), // 此处无法获取item对应的indexPath,逻辑无法实现 )方案C:结合估算尺寸
不清楚如何同时设置估算尺寸与分数宽度,也不知道该怎么和sizeForItemAt配合使用:widthDimension: estimated: .fractionalWidth(1/3), ... groupSize .. similar idea
理想需求
多数列表项保持两列布局,偶尔根据数据源标记,将指定项设置为单列全屏宽度。
补充:屏幕旋转的布局适配
实际开发中需处理屏幕旋转等布局变化,组合布局可优雅实现适配:
// 更精准的方式是获取容器尺寸,适配底部弹窗等场景 var columnCount = CGFloat(2) if let containerBounds = self.findViewController()?.view.bounds { if containerBounds.width > containerBounds.height { columnCount = CGFloat(3) } } let layoutSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1/columnCount), heightDimension: .absolute(rowHeight) )
记得添加布局失效逻辑,确保旋转时重新计算布局:
override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() collection.collectionViewLayout.invalidateLayout() // 触发布局重新计算 }
解决方案:动态生成分组布局
要实现特殊项全屏、普通项多列的效果,核心是利用组合布局的动态分组能力,结合数据源判断生成不同尺寸的布局单元。
具体实现代码
// 假设数据源为daData,rowHeight为固定行高 let layout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in guard let self = self else { return nil } // 动态适配列数(支持旋转) var columnCount = CGFloat(2) let containerSize = layoutEnvironment.container.contentSize if containerSize.width > containerSize.height { columnCount = CGFloat(3) } // 定义两种item尺寸:普通项和特殊项 let regularItemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1/columnCount), heightDimension: .absolute(rowHeight) ) let regularItem = NSCollectionLayoutItem(layoutSize: regularItemSize) let fullWidthItemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(rowHeight) ) let fullWidthItem = NSCollectionLayoutItem(layoutSize: fullWidthItemSize) // 分组尺寸:行高固定,宽度占满容器 let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(rowHeight) ) // 按数据源生成对应布局item var items = [NSCollectionLayoutItem]() for data in self.daData { items.append(data.whatever == .special ? fullWidthItem : regularItem) } // 分组逻辑:连续普通项按列数分组,特殊项单独成组 var groups = [NSCollectionLayoutGroup]() var currentRegularItems = [NSCollectionLayoutItem]() for item in items { let isFullWidth = item.layoutSize.widthDimension == .fractionalWidth(1.0) if isFullWidth { // 先处理积累的普通项分组 if !currentRegularItems.isEmpty { let regularGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: currentRegularItems) groups.append(regularGroup) currentRegularItems.removeAll() } // 特殊项单独成组 let fullWidthGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) groups.append(fullWidthGroup) } else { currentRegularItems.append(item) // 普通项数量达到列数时分组 if currentRegularItems.count == Int(columnCount) { let regularGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: currentRegularItems) groups.append(regularGroup) currentRegularItems.removeAll() } } } // 处理剩余的普通项 if !currentRegularItems.isEmpty { let regularGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: currentRegularItems) groups.append(regularGroup) } // 垂直组合所有分组,生成最终分区布局 let section = NSCollectionLayoutSection(group: NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: groups)) return section }
核心思路
- 预先定义普通项和特殊项两种布局尺寸;
- 遍历数据源,将每个数据项映射到对应的布局item;
- 对item进行分组:连续普通项按列数组成水平分组,特殊项单独组成水平分组;
- 将所有分组组合成垂直分组,作为分区的核心布局组;
- 结合屏幕尺寸动态调整列数,同时通过
invalidateLayout触发布局更新,适配旋转场景。
内容的提问来源于stack exchange,提问作者Fattie
相关产品推荐
相关产品推荐

