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

Swift中如何为基于PinterestLayout的UICollectionView添加头部视图

如何给基于PinterestLayout的UICollectionView添加头部视图

我之前在做瀑布流布局时也遇到过这个问题——毕竟PinterestLayout是自定义的UICollectionViewLayout,不像系统的UICollectionViewFlowLayout那样自带头部视图的支持,得自己手动扩展布局逻辑。下面给你一步步拆解实现方法:

1. 扩展PinterestLayout,添加头部支持

首先要在你的PinterestLayout类里,加入头部视图的布局计算逻辑,核心是处理头部的尺寸、位置,并且把头部高度纳入到瀑布流的整体布局中:

步骤1:定义代理协议,新增头部尺寸方法

我们需要给PinterestLayout加个代理,让外部可以指定每个section的头部大小,同时保留原来瀑布流需要的item高度方法:

protocol PinterestLayoutDelegate: AnyObject {
    // 原瀑布流需要的item高度方法
    func collectionView(_ collectionView: UICollectionView, layout pinterestLayout: PinterestLayout, heightForItemAt indexPath: IndexPath) -> CGFloat
    // 新增:头部尺寸方法
    func collectionView(_ collectionView: UICollectionView, layout pinterestLayout: PinterestLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
}

步骤2:修改PinterestLayout核心逻辑

在布局类里添加存储头部布局属性的数组,并重写关键方法来处理头部:

class PinterestLayout: UICollectionViewLayout {
    weak var delegate: PinterestLayoutDelegate?
    
    // 存储头部的布局属性
    private var headerAttributes: [UICollectionViewLayoutAttributes] = []
    // 原有的瀑布流属性
    var columnCount = 2
    var cellPadding: CGFloat = 8
    private var cache: [UICollectionViewLayoutAttributes] = []
    private var contentHeight: CGFloat = 0
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }
    
    override func prepare() {
        super.prepare()
        guard cache.isEmpty, let collectionView = collectionView else { return }
        
        cache.removeAll()
        headerAttributes.removeAll()
        contentHeight = 0
        
        let columnWidth = contentWidth / CGFloat(columnCount)
        var xOffsets = (0..<columnCount).map { CGFloat($0) * columnWidth }
        var yOffsets = Array(repeating: 0.0, count: columnCount)
        
        // 遍历每个section,先处理头部,再处理item
        for section in 0..<collectionView.numberOfSections {
            // 获取当前section的头部尺寸
            let headerSize = delegate?.collectionView(collectionView, layout: self, referenceSizeForHeaderInSection: section) ?? .zero
            if headerSize.height > 0 {
                // 计算头部的frame(宽度占满collectionView,高度由代理返回)
                let headerFrame = CGRect(
                    x: collectionView.contentInset.left,
                    y: contentHeight,
                    width: contentWidth,
                    height: headerSize.height
                )
                let headerAttrs = UICollectionViewLayoutAttributes(
                    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                    with: IndexPath(section: section, item: 0)
                )
                headerAttrs.frame = headerFrame
                headerAttributes.append(headerAttrs)
                
                // 更新内容高度和item的Y偏移,让item从头部下方开始排列
                contentHeight += headerSize.height
                yOffsets = Array(repeating: contentHeight, count: columnCount)
            }
            
            // 处理当前section的items(原瀑布流逻辑)
            let itemCount = collectionView.numberOfItems(inSection: section)
            var column = 0
            for item in 0..<itemCount {
                let indexPath = IndexPath(section: section, item: item)
                let itemHeight = delegate?.collectionView(collectionView, layout: self, heightForItemAt: indexPath) ?? 100
                let cellHeight = cellPadding * 2 + itemHeight
                
                let frame = CGRect(
                    x: xOffsets[column],
                    y: yOffsets[column],
                    width: columnWidth,
                    height: cellHeight
                )
                let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
                
                let cellAttrs = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                cellAttrs.frame = insetFrame
                cache.append(cellAttrs)
                
                // 更新内容高度和当前列的Y偏移
                contentHeight = max(contentHeight, frame.maxY)
                yOffsets[column] += cellHeight
                
                // 找到Y偏移最小的列,下一个item放在这里
                column = yOffsets.firstIndex(of: yOffsets.min()!)!
            }
        }
    }
    
    // 返回所有可见的布局属性(包括头部和cell)
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleAttrs = [UICollectionViewLayoutAttributes]()
        
        // 筛选可见的头部
        for headerAttr in headerAttributes {
            if headerAttr.frame.intersects(rect) {
                visibleAttrs.append(headerAttr)
            }
        }
        // 筛选可见的cell
        for cellAttr in cache {
            if cellAttr.frame.intersects(rect) {
                visibleAttrs.append(cellAttr)
            }
        }
        
        return visibleAttrs
    }
    
    // 单独返回头部的布局属性,collectionView会调用这个方法获取头部的布局信息
    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        if elementKind == UICollectionView.elementKindSectionHeader {
            return headerAttributes.first { $0.indexPath.section == indexPath.section }
        }
        return nil
    }
    
    // 返回整体内容大小,要包含头部的高度
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
}

2. 在ViewController中实现头部相关方法

这部分和你用FlowLayout时的逻辑类似,但要注意适配PinterestLayout的代理:

步骤1:注册头部视图和配置collectionView

class YourViewController: UIViewController, UICollectionViewDataSource, PinterestLayoutDelegate {
    private var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView()
    }
    
    private func setupCollectionView() {
        let layout = PinterestLayout()
        layout.delegate = self
        layout.columnCount = 2
        layout.cellPadding = 8
        
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.contentInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
        
        // 注册头部视图
        collectionView.register(SectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "SectionHeader")
        // 注册cell
        collectionView.register(PinterestCell.self, forCellWithReuseIdentifier: "PinterestCell")
        
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
    }
}

步骤2:实现数据源和代理方法

// MARK: - UICollectionViewDataSource
extension YourViewController {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 3 // 示例section数量
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 8 // 示例item数量
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PinterestCell", for: indexPath) as! PinterestCell
        // 配置你的cell内容,比如图片、文字等
        cell.backgroundColor = .white
        cell.layer.cornerRadius = 8
        cell.layer.shadowOpacity = 0.1
        cell.layer.shadowOffset = .zero
        return cell
    }
    
    // 创建头部视图
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard kind == UICollectionView.elementKindSectionHeader else {
            return UICollectionReusableView()
        }
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "SectionHeader", for: indexPath) as! SectionHeaderView
        // 配置头部内容,比如标题
        header.titleLabel.text = "Section \(indexPath.section + 1)"
        return header
    }
}

// MARK: - PinterestLayoutDelegate
extension YourViewController {
    // 返回每个item的高度(瀑布流必需)
    func collectionView(_ collectionView: UICollectionView, layout pinterestLayout: PinterestLayout, heightForItemAt indexPath: IndexPath) -> CGFloat {
        // 这里可以根据实际内容返回高度,示例用随机高度
        return CGFloat(Int.random(in: 120...280))
    }
    
    // 返回每个section的头部尺寸
    func collectionView(_ collectionView: UICollectionView, layout pinterestLayout: PinterestLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        // 示例:头部高度50,宽度占满collectionView(减去contentInset)
        return CGSize(width: pinterestLayout.contentWidth, height: 50)
    }
}

步骤3:定义头部视图类

class SectionHeaderView: UICollectionReusableView {
    let titleLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUI()
    }
    
    private func setupUI() {
        backgroundColor = .systemGray5
        titleLabel.font = UIFont.boldSystemFont(ofSize: 16)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        addSubview(titleLabel)
        
        NSLayoutConstraint.activate([
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }
}

关键注意点

  • 必须重写layoutAttributesForSupplementaryView(ofKind:at:):这个方法是collectionView获取头部布局信息的关键,如果不实现,头部视图会无法显示。
  • 头部高度要影响item的布局:在prepare()方法里,计算完头部后要更新yOffsets,确保section里的item从头部下方开始排列,而不是覆盖在头部上面。
  • 内容高度要包含头部:在计算collectionViewContentSize时,要把所有头部的高度都加进去,否则底部的内容可能无法滚动显示。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 03:54:11