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
相关产品推荐
相关产品推荐

