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

如何通过纯代码创建自定义UICollectionViewFlowLayout

纯代码实现自定义CollectionViewLayout替代FlowLayout的方案

很简单,我们只需要替换布局的初始化方式,同时补全基类里未完成的代码逻辑就可以了,具体步骤如下:

1. 给自定义布局添加纯代码初始化器

原来的DiscoverCollection只有init?(coder:)(用于Storyboard加载),纯代码场景需要我们手动添加一个无参初始化方法:

import UIKit
private let kReducedHeightColumnIndex = 1
private let kItemHeightAspect: CGFloat = 2

class DiscoverCollection: BaseCollectionViewLayout {
    private var _itemSize : CGSize!
    private var _columnsXoffset : [CGFloat]!
    
    // 新增纯代码初始化器
    init() {
        super.init()
        self.totalColumns = 3
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.totalColumns = 3
    }
    
    private func isLastItemSingleInRow(_ indexPath: IndexPath) -> Bool {
        return indexPath.item == (totalItemsInSection - 1) && indexPath.item % totalColumns == 0
    }
    
    override func calculateItemSize() {
        let contentWidthWithoutIndents = collectionView!.bounds.width - contentInsets.left - contentInsets.right
        let itemWidth = (contentWidthWithoutIndents - (CGFloat(totalColumns) - 1) * interItemsSpacing) / CGFloat(totalColumns)
        let itemHeight = itemWidth * kItemHeightAspect // 修复高度计算,匹配定义的高宽比
        _itemSize = CGSize(width: itemWidth, height: itemHeight)
        _columnsXoffset = []
        for columnIndex in 0..<totalColumns {
            // 加上左侧内边距,保证布局不溢出
            _columnsXoffset.append(contentInsets.left + CGFloat(columnIndex) * (_itemSize.width + interItemsSpacing))
        }
    }
    
    override func columnIndexForItemAt(indexPath: IndexPath) -> Int {
        let columnIndex = indexPath.item % totalColumns
        return self.isLastItemSingleInRow(indexPath) ? kReducedHeightColumnIndex : columnIndex
    }
    
    override func calculateItemFrame(indexPath: IndexPath, columnIndex: Int, columnYoffset: CGFloat) -> CGRect {
        let rowIndex = indexPath.item / totalColumns
        let halfItemHeight = (_itemSize.height - interItemsSpacing) / 2
        var itemHeight = _itemSize.height
        if (rowIndex == 0 && columnIndex == kReducedHeightColumnIndex) || self.isLastItemSingleInRow(indexPath) {
            itemHeight = halfItemHeight
        }
        return CGRect(x: _columnsXoffset[columnIndex], y: columnYoffset, width: _itemSize.width, height: itemHeight)
    }
}

2. 替换ViewController中的布局创建代码

把原来创建UICollectionViewFlowLayout的代码,换成初始化你的自定义DiscoverCollection:

/** Collection View **/
// 初始化自定义布局,替代FlowLayout
let customLayout = DiscoverCollection()
// 按需设置布局的自定义属性,比如item间距
customLayout.interItemsSpacing = 8

// 创建CollectionView时传入这个自定义布局
self.collectionView = UICollectionView(
    frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height),
    collectionViewLayout: customLayout
)
collectionView.register(DiscoverCVC.self, forCellWithReuseIdentifier: cellId)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .clear
self.addSubview(collectionView)

3. 补全BaseCollectionViewLayout的prepare方法(关键!)

你提供的基类代码中prepare()方法被截断了,这会导致布局完全无法工作,需要补全这个核心方法:

import UIKit

class BaseCollectionViewLayout: UICollectionViewLayout {
    private var _layoutMap = [IndexPath : UICollectionViewLayoutAttributes]()
    private var _columnsYoffset : [CGFloat]!
    private var _contentSize : CGSize!
    private(set) var totalItemsInSection = 0
    var totalColumns = 0
    var interItemsSpacing : CGFloat = 8
    var contentInsets : UIEdgeInsets {
        return collectionView!.contentInset
    }
    
    override var collectionViewContentSize: CGSize {
        return _contentSize
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var layoutAttributesArray = [UICollectionViewLayoutAttributes]()
        for(_, layoutAttributes) in _layoutMap {
            if rect.intersects(layoutAttributes.frame) {
                layoutAttributesArray.append(layoutAttributes)
            }
        }
        return layoutAttributesArray
    }
    
    func columnIndexForItemAt(indexPath: IndexPath) -> Int {
        return indexPath.item % totalColumns
    }
    
    func calculateItemFrame(indexPath: IndexPath, columnIndex: Int, columnYoffset: CGFloat) -> CGRect {
        return CGRect.zero
    }
    
    func calculateItemSize(){}
    
    override func prepare() {
        _layoutMap.removeAll()
        guard totalColumns > 0, let collectionView = collectionView else { return }
        
        // 初始化列的Y偏移,从顶部内边距开始
        _columnsYoffset = Array(repeating: contentInsets.top, count: totalColumns)
        totalItemsInSection = collectionView.numberOfItems(inSection: 0)
        
        if totalItemsInSection > 0 {
            calculateItemSize()
            var contentSizeHeight: CGFloat = contentInsets.top
            
            for itemIndex in 0..<totalItemsInSection {
                let indexPath = IndexPath(item: itemIndex, section: 0)
                let columnIndex = columnIndexForItemAt(indexPath: indexPath)
                let columnYOffset = _columnsYoffset[columnIndex]
                
                // 计算当前cell的frame
                let itemFrame = calculateItemFrame(indexPath: indexPath, columnIndex: columnIndex, columnYoffset: columnYOffset)
                // 创建并保存布局属性
                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                attributes.frame = itemFrame
                _layoutMap[indexPath] = attributes
                
                // 更新当前列的Y偏移(加上item间距)
                _columnsYoffset[columnIndex] = itemFrame.maxY + interItemsSpacing
                // 记录最大的Y值,作为内容高度的基准
                contentSizeHeight = max(contentSizeHeight, itemFrame.maxY)
            }
            
            // 最终内容尺寸要加上底部内边距
            _contentSize = CGSize(
                width: collectionView.bounds.width,
                height: contentSizeHeight + contentInsets.bottom
            )
        }
    }
}

这样一来,你的自定义布局就能完全替代FlowLayout,实现和Storyboard配置时一样的效果了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 06:41:32