如何实现UICollectionView纵向填充单元格,支持动态行数与垂直滚动?
实现UICollectionView纵向按列填充(垂直滚动)的解决方案
嘿,这个需求我之前做电商列表项目的时候刚好碰到过!要让UICollectionView从默认的横向按行填充改成纵向按列填充(也就是先填满第一列的所有行,再开始填充第二列),同时满足垂直滚动、不修改单元格尺寸、动态确定行数的要求,核心就是自定义UICollectionViewFlowLayout来重写布局逻辑。
核心思路
默认的UICollectionViewFlowLayout是按行优先排列的,我们需要重写它的layoutAttributesForElements(in:)方法,手动计算每个cell的位置,改成列优先的排列规则。同时通过collectionView的可用宽度动态计算列数,再结合总item数自动推导出行数,完全不需要硬编码。
自定义FlowLayout代码实现
class VerticalFillFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // 获取系统默认计算的布局属性 guard let originalAttributes = super.layoutAttributesForElements(in: rect), let collectionView = collectionView else { return originalAttributes } // 1. 动态计算列数:根据collectionView宽度、cell尺寸和间距自动算出 let availableWidth = collectionView.bounds.width - sectionInset.left - sectionInset.right let columnTotalWidth = itemSize.width + minimumInteritemSpacing let columnCount = max(1, Int(availableWidth / columnTotalWidth)) // 2. 记录每一列当前的底部Y坐标(用来放置下一个cell) var columnBottomY = Array(repeating: sectionInset.top, count: columnCount) // 3. 遍历所有cell,重新设置它们的位置 for attributes in originalAttributes { // 只处理cell的布局,忽略header/footer等元素 guard attributes.representedElementCategory == .cell else { continue } // 找到当前最矮的列(保证列优先填充,同时尽量平衡各列高度) let shortestColumnIndex = columnBottomY.firstIndex(of: columnBottomY.min()!)! // 计算当前cell的X、Y坐标 let cellX = sectionInset.left + CGFloat(shortestColumnIndex) * (itemSize.width + minimumInteritemSpacing) let cellY = columnBottomY[shortestColumnIndex] // 更新cell的frame attributes.frame = CGRect(x: cellX, y: cellY, width: itemSize.width, height: itemSize.height) // 更新该列的底部Y坐标(加上当前cell高度和行间距) columnBottomY[shortestColumnIndex] = cellY + itemSize.height + minimumLineSpacing } // 4. 更新collectionView的滚动范围,确保垂直滚动能覆盖所有内容 let maxColumnHeight = columnBottomY.max()! collectionViewContentSize = CGSize( width: collectionView.bounds.width, height: maxColumnHeight - minimumLineSpacing + sectionInset.bottom ) return originalAttributes } // 当collectionView尺寸变化时(比如屏幕旋转),重新计算布局 override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } }
关键细节说明
- 动态列数计算:通过可用宽度除以单列总宽度(cell宽+列间距),自动适配不同屏幕尺寸,不用固定写死列数。
- 列优先填充逻辑:每次选择当前高度最低的列放置下一个cell,完美实现“先列后行”的填充顺序,同时避免某一列过高的问题。
- 自动行数推导:总item数除以列数(向上取整)就是行数,完全由内容和列数动态决定,不需要手动配置。
- 滚动范围适配:最后更新
collectionViewContentSize,确保垂直滚动的范围刚好覆盖所有cell,不会出现滚动不全或者多余空白的情况。
如何使用
在创建UICollectionView时,直接替换成我们自定义的布局即可,cell尺寸按照你的需求设置就行:
// 初始化自定义布局 let verticalLayout = VerticalFillFlowLayout() verticalLayout.itemSize = CGSize(width: 120, height: 150) // 你的cell原始尺寸,不用修改 verticalLayout.minimumInteritemSpacing = 12 // 列与列之间的间距 verticalLayout.minimumLineSpacing = 12 // 同一列中cell之间的间距 verticalLayout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) // 整体内边距 // 创建collectionView let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: verticalLayout) collectionView.dataSource = self collectionView.delegate = self collectionView.register(YourCustomCell.self, forCellWithReuseIdentifier: "YourCellID") view.addSubview(collectionView)
这样就能完美实现你要的效果啦!
内容的提问来源于stack exchange,提问作者vignan kumar
相关产品推荐
相关产品推荐

