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

如何实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 03:58:45