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

自定义叠加布局UICollectionView滚动时单元格消失问题求助

解决UICollectionView滚动时单元格位置异常问题

问题根源

  1. 直接在cellForItemAt中修改cell.frame会破坏UICollectionViewFlowLayout的自动布局逻辑,单元格复用后位置计算完全混乱
  2. 当前设置的负minimumLineSpacing会导致组与组之间的间距计算错误,滚动时布局偏移加剧
  3. 手动计算的rowOffset没有结合UICollectionView的滚动状态,无法适配滚动时的位置更新

解决方案

移除手动修改cell frame的代码,通过自定义UICollectionViewFlowLayout来精准控制每个单元格的位置,让布局逻辑完全由布局类管理,避免与UICollectionView的复用机制冲突。

修改后的完整代码

import UIKit

class SpiralCollectionLayout: UICollectionViewFlowLayout {
    // 每组包含5个item
    private let itemsPerGroup = 5
    // 每组的高度,基于collectionView宽度计算
    private var groupHeight: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        return collectionView.frame.width
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let originalAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        var adjustedAttributes = [UICollectionViewLayoutAttributes]()
        
        for attributes in originalAttributes {
            guard let copy = attributes.copy() as? UICollectionViewLayoutAttributes else { continue }
            adjustLayoutAttributes(copy)
            adjustedAttributes.append(copy)
        }
        
        return adjustedAttributes
    }
    
    private func adjustLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) {
        guard let collectionView = collectionView else { return }
        let itemIndex = attributes.indexPath.item
        let groupIndex = itemIndex / itemsPerGroup
        let itemInGroup = itemIndex % itemsPerGroup
        let groupYOffset = CGFloat(groupIndex) * groupHeight
        
        let width = collectionView.frame.width
        // 根据组内位置设置每个item的frame
        switch itemInGroup {
        case 0:
            attributes.frame = CGRect(x: 0, y: groupYOffset, width: width * 2/3, height: width * 1/3)
        case 1:
            attributes.frame = CGRect(x: width * 2/3, y: groupYOffset, width: width * 1/3, height: width * 2/3)
        case 2:
            attributes.frame = CGRect(x: 0, y: groupYOffset + width * 1/3, width: width * 1/3, height: width * 2/3)
        case 3:
            attributes.frame = CGRect(x: width * 1/3, y: groupYOffset + width * 2/3, width: width * 1/3, height: width * 1/3)
        case 4:
            attributes.frame = CGRect(x: width * 1/3, y: groupYOffset + width * 1/3, width: width * 2/3, height: width * 1/3)
        default:
            break
        }
    }
    
    override func collectionViewContentSize() -> CGSize {
        guard let collectionView = collectionView else { return .zero }
        let totalGroups = (collectionView.numberOfItems(inSection: 0) + itemsPerGroup - 1) / itemsPerGroup
        return CGSize(width: collectionView.frame.width, height: CGFloat(totalGroups) * groupHeight)
    }
}

class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
    
    var collectionView: UICollectionView!

    let colors = [UIColor.yellow, UIColor.red, UIColor.blue, UIColor.green, UIColor.purple, UIColor.systemRed, UIColor.systemMint, UIColor.systemBlue, UIColor.cyan, UIColor.gray, UIColor.systemTeal, UIColor.systemYellow]
    
    func generateRandomNumber(min: Int, max: Int) -> Int {
        return Int.random(in: min...max)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 使用自定义布局
        let layout = SpiralCollectionLayout()
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        layout.sectionInset = .zero
  
        collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.backgroundColor = .white
        view.addSubview(collectionView)
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 300
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        let randomNumber = generateRandomNumber(min: 0, max: colors.count-1)
        cell.backgroundColor = colors[randomNumber]
        // 移除手动修改frame的代码
        return cell
    }
}

关键改动说明

  1. 自定义SpiralCollectionLayout,重写layoutAttributesForElements(in:)和collectionViewContentSize()方法,完全接管布局计算
  2. 按每5个item为一组,计算每组的Y轴偏移,然后为组内每个item设置精准的frame
  3. 移除cellForItemAt中所有手动修改frame的代码,避免干扰布局逻辑
  4. 自定义布局中直接计算每个item的尺寸和位置,无需再实现UICollectionViewDelegateFlowLayout的sizeForItemAt方法

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 19:39:24