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

Swift实现:添加元素时动态调整UICollectionViewCell布局宽度

Hey there! 作为Swift新手想要实现这种循环式的UICollectionView动态布局,我来给你梳理一个清晰的解决方案~

核心实现思路

这种非标准的流式布局,系统自带的UICollectionViewFlowLayout很难满足需求,我们需要自定义一个UICollectionViewLayout,根据元素总数量对3取模的结果,来切换三种布局模式(1个、2个、3个元素的布局),然后逐个计算每个cell的frame即可。

第一步:自定义布局类的基础结构

先创建一个继承自UICollectionViewLayout的类,定义布局类型枚举和必要属性:

enum LayoutType {
    case singleItem   // 1个元素的布局
    case twoItems     // 2个元素的布局
    case threeItems   // 3个元素的布局
}

class CustomCycleLayout: UICollectionViewLayout {
    // 缓存所有cell的布局属性,避免重复计算
    private var layoutAttributesCache = [UICollectionViewLayoutAttributes]()
    // 布局内容的总宽度和高度
    private var contentWidth: CGFloat = 0
    private var contentHeight: CGFloat = 0
    
    // 根据元素总数判断当前应该使用哪种布局
    private var currentLayoutType: LayoutType {
        guard let itemCount = collectionView?.numberOfItems(inSection: 0) else {
            return .singleItem
        }
        let remainder = itemCount % 3
        switch remainder {
        case 1:
            return .singleItem
        case 2:
            return .twoItems
        default: // 余数为0时,对应3个元素的布局
            return .threeItems
        }
    }
    
    // 返回布局的整体尺寸
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
}

第二步:计算每个cell的布局位置

重写prepare()方法,在这里根据当前布局类型,逐个计算每个cell的frame并缓存:

override func prepare() {
    super.prepare()
    layoutAttributesCache.removeAll()
    guard let collectionView = collectionView else { return }
    
    contentWidth = collectionView.bounds.width
    let spacing: CGFloat = 8 // 元素间的间距,可根据设计调整
    let itemCount = collectionView.numberOfItems(inSection: 0)
    
    switch currentLayoutType {
    case .singleItem:
        // 单个元素:占满宽度,高度设为宽度的0.75(可自定义比例)
        let itemWidth = contentWidth - spacing * 2
        let itemHeight = itemWidth * 0.75
        let frame = CGRect(x: spacing, y: spacing, width: itemWidth, height: itemHeight)
        let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0))
        attributes.frame = frame
        layoutAttributesCache.append(attributes)
        contentHeight = itemHeight + spacing * 2
        
    case .twoItems:
        // 两个元素:左右并排,宽度各占一半减间距
        let itemWidth = (contentWidth - spacing * 3) / 2
        let itemHeight = itemWidth * 0.75
        // 第一个cell
        let firstFrame = CGRect(x: spacing, y: spacing, width: itemWidth, height: itemHeight)
        let firstAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0))
        firstAttributes.frame = firstFrame
        layoutAttributesCache.append(firstAttributes)
        // 第二个cell
        let secondFrame = CGRect(x: itemWidth + spacing * 2, y: spacing, width: itemWidth, height: itemHeight)
        let secondAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 1, section: 0))
        secondAttributes.frame = secondFrame
        layoutAttributesCache.append(secondAttributes)
        contentHeight = itemHeight + spacing * 2
        
    case .threeItems:
        // 三个元素:顶部一个占满宽度,下方两个左右并排
        // 顶部cell
        let topItemWidth = contentWidth - spacing * 2
        let topItemHeight = topItemWidth * 0.5
        let topFrame = CGRect(x: spacing, y: spacing, width: topItemWidth, height: topItemHeight)
        let topAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0))
        topAttributes.frame = topFrame
        layoutAttributesCache.append(topAttributes)
        // 下方两个cell
        let bottomItemWidth = (contentWidth - spacing * 3) / 2
        let bottomItemHeight = bottomItemWidth * 0.75
        let bottomY = topItemHeight + spacing * 2
        // 第二个cell
        let secondFrame = CGRect(x: spacing, y: bottomY, width: bottomItemWidth, height: bottomItemHeight)
        let secondAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 1, section: 0))
        secondAttributes.frame = secondFrame
        layoutAttributesCache.append(secondAttributes)
        // 第三个cell
        let thirdFrame = CGRect(x: bottomItemWidth + spacing * 2, y: bottomY, width: bottomItemWidth, height: bottomItemHeight)
        let thirdAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 2, section: 0))
        thirdAttributes.frame = thirdFrame
        layoutAttributesCache.append(thirdAttributes)
        contentHeight = bottomY + bottomItemHeight + spacing
    }
}

第三步:返回布局属性给CollectionView

重写两个方法,返回我们缓存的布局属性:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    var visibleAttributes = [UICollectionViewLayoutAttributes]()
    for attributes in layoutAttributesCache {
        if attributes.frame.intersects(rect) {
            visibleAttributes.append(attributes)
        }
    }
    return visibleAttributes
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return layoutAttributesCache[indexPath.item]
}

第四步:在ViewController中使用自定义布局

最后在你的页面里,初始化CollectionView时使用这个自定义布局即可:

class DemoViewController: UIViewController, UICollectionViewDataSource {
    private var collectionView: UICollectionView!
    private var dataSource = [String]() // 你的数据源
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        // 添加添加元素的按钮
        let addBtn = UIButton(type: .system)
        addBtn.setTitle("添加元素", for: .normal)
        addBtn.addTarget(self, action: #selector(addNewItem), for: .touchUpInside)
        addBtn.frame = CGRect(x: 20, y: view.safeAreaInsets.top + 20, width: view.bounds.width - 40, height: 44)
        view.addSubview(addBtn)
        
        // 初始化CollectionView
        let customLayout = CustomCycleLayout()
        collectionView = UICollectionView(frame: CGRect(x: 0, y: view.safeAreaInsets.top + 80, width: view.bounds.width, height: view.bounds.height - view.safeAreaInsets.top - 80), collectionViewLayout: customLayout)
        collectionView.dataSource = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "CycleCell")
        collectionView.backgroundColor = .white
        view.addSubview(collectionView)
    }
    
    @objc private func addNewItem() {
        dataSource.append("Item \(dataSource.count + 1)")
        collectionView.reloadData()
    }
    
    // MARK: - UICollectionViewDataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CycleCell", for: indexPath)
        cell.backgroundColor = .systemTeal
        // 添加文本标签
        if let label = cell.viewWithTag(100) as? UILabel {
            label.text = dataSource[indexPath.item]
        } else {
            let label = UILabel(frame: cell.bounds)
            label.tag = 100
            label.textAlignment = .center
            label.textColor = .white
            label.font = .systemFont(ofSize: 16)
            cell.addSubview(label)
            label.text = dataSource[indexPath.item]
        }
        return cell
    }
}

可调整的细节

  • 所有的间距、元素宽高比例都可以根据你的设计图修改,比如单个元素的高度、三个元素时顶部元素的高度等。
  • 如果你的需求是每3个元素为一组循环布局(比如4个元素是3个的布局+1个的布局,5个是3+2,6个是3+3),只需要修改prepare()方法,循环计算每组的位置并累加contentHeight即可,逻辑类似上面的单组计算。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 10:09:15