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

如何用Decorator View实现UICollectionView目标设计及添加内部分隔线

嘿,我来帮你搞定这几个UICollectionView的问题,都是实际开发中经常碰到的场景,咱们逐个拆解:

一、添加内部分隔线

推荐两种靠谱的实现方式,按需选择:

方式1:自定义Cell时内置分隔线

这种方式最直观,在Cell的contentView里添加UIView作为分隔线,然后根据Cell的位置动态控制显示/隐藏:

class CustomCollectionViewCell: UICollectionViewCell {
    // 左分隔线、底部分隔线(按需添加右/上分隔线)
    let leftSeparator = UIView()
    let bottomSeparator = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupSeparators()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupSeparators()
    }
    
    private func setupSeparators() {
        leftSeparator.backgroundColor = .lightGray
        bottomSeparator.backgroundColor = .lightGray
        
        contentView.addSubview(leftSeparator)
        contentView.addSubview(bottomSeparator)
        
        // 布局分隔线
        leftSeparator.translatesAutoresizingMaskIntoConstraints = false
        bottomSeparator.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            leftSeparator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            leftSeparator.topAnchor.constraint(equalTo: contentView.topAnchor),
            leftSeparator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            leftSeparator.widthAnchor.constraint(equalToConstant: 0.5),
            
            bottomSeparator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            bottomSeparator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            bottomSeparator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            bottomSeparator.heightAnchor.constraint(equalToConstant: 0.5)
        ])
    }
    
    // 根据位置更新分隔线显示状态
    func updateSeparators(for indexPath: IndexPath, itemsPerRow: Int, totalItemsInSection: Int) {
        // 左分隔线:第一列隐藏,其余显示
        leftSeparator.isHidden = indexPath.item % itemsPerRow == 0
        
        // 底部分隔线:最后一行隐藏,其余显示
        let lastRowStartIndex = totalItemsInSection - (totalItemsInSection % itemsPerRow)
        let adjustedLastRowStart = totalItemsInSection % itemsPerRow == 0 ? lastRowStartIndex - itemsPerRow : lastRowStartIndex
        bottomSeparator.isHidden = indexPath.item >= adjustedLastRowStart
    }
}

然后在cellForItemAt里调用这个方法:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCollectionViewCell
    let itemsPerRow = calculateItemsPerRow() // 这个方法后面讲动态行数量时实现
    let totalItems = collectionView.numberOfItems(inSection: indexPath.section)
    cell.updateSeparators(for: indexPath, itemsPerRow: itemsPerRow, totalItemsInSection: totalItems)
    // 其他Cell配置逻辑
    return cell
}

方式2:自定义FlowLayout绘制分隔线

如果不想侵入Cell,也可以在布局里直接画分隔线,重写draw(_ rect:)方法:

class CustomFlowLayout: UICollectionViewFlowLayout {
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let collectionView = collectionView else { return }
        let itemsPerRow = calculateItemsPerRow()
        let sections = 0..<collectionView.numberOfSections
        
        for section in sections {
            let totalItems = collectionView.numberOfItems(inSection: section)
            guard totalItems > 0 else { continue }
            
            // 计算最后一行的起始位置
            let lastRowStartIndex = totalItems - (totalItems % itemsPerRow)
            let adjustedLastRowStart = totalItems % itemsPerRow == 0 ? lastRowStartIndex - itemsPerRow : lastRowStartIndex
            
            for item in 0..<totalItems {
                let indexPath = IndexPath(item: item, section: section)
                guard let attributes = layoutAttributesForItem(at: indexPath) else { continue }
                let cellFrame = attributes.frame
                
                // 绘制左分隔线(第一列跳过)
                if item % itemsPerRow != 0 {
                    let leftLineFrame = CGRect(x: cellFrame.minX, y: cellFrame.minY, width: 0.5, height: cellFrame.height)
                    UIColor.lightGray.setFill()
                    UIRectFill(leftLineFrame)
                }
                
                // 绘制底部分隔线(最后一行跳过)
                if item < adjustedLastRowStart {
                    let bottomLineFrame = CGRect(x: cellFrame.minX, y: cellFrame.maxY, width: cellFrame.width, height: 0.5)
                    UIColor.lightGray.setFill()
                    UIRectFill(bottomLineFrame)
                }
            }
        }
    }
    
    // 布局变化时重新绘制分隔线
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}
二、实现每行Cell数量动态变化

核心是根据CollectionView的宽度(或屏幕尺寸、自定义规则)实时计算每行能容纳的Cell数量,然后更新ItemSize。自定义FlowLayout是最佳实践:

class DynamicFlowLayout: UICollectionViewFlowLayout {
    // 可以自定义最小Cell宽度,适配不同屏幕
    private let minItemWidth: CGFloat = 150
    
    override func prepare() {
        super.prepare()
        guard let collectionView = collectionView else { return }
        
        let itemsPerRow = calculateItemsPerRow()
        let availableWidth = collectionView.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(itemsPerRow - 1)
        let itemWidth = floor(availableWidth / CGFloat(itemsPerRow))
        
        // 固定高度或根据内容动态计算高度
        itemSize = CGSize(width: itemWidth, height: 100)
    }
    
    private func calculateItemsPerRow() -> Int {
        guard let collectionView = collectionView else { return 1 }
        
        let totalAvailableWidth = collectionView.bounds.width - sectionInset.left - sectionInset.right
        // 计算最多能放多少个Cell(每个Cell至少minItemWidth,加上间距)
        let maxItems = Int(floor((totalAvailableWidth + minimumInteritemSpacing) / (minItemWidth + minimumInteritemSpacing)))
        return max(maxItems, 1) // 至少显示1个
    }
    
    // 屏幕旋转或尺寸变化时重新计算布局
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        guard let collectionView = collectionView else { return false }
        return newBounds.width != collectionView.bounds.width
    }
}

然后给CollectionView设置这个布局:

let layout = DynamicFlowLayout()
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
collectionView.collectionViewLayout = layout
三、最后一行隐藏底部分隔线

这个逻辑其实已经包含在上面的两个分隔线实现里了,核心是判断当前Cell是否属于所在Section的最后一行:

  1. 先计算当前Section的总Item数totalItems
  2. 根据每行数量itemsPerRow,算出最后一行的起始Item索引:
    • 如果totalItems % itemsPerRow == 0,说明最后一行刚好填满,起始索引是totalItems - itemsPerRow
    • 否则起始索引是totalItems - (totalItems % itemsPerRow)
  3. 只要当前Cell的indexPath.item >= 最后一行起始索引,就隐藏底部分隔线

比如在Cell的updateSeparators方法里已经实现了这个判断,自定义FlowLayout的绘制逻辑里也做了同样的判断,直接复用即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 09:56:31