如何为多分区UICollectionViewCompositionalLayout实现弹性头部
UICollectionViewCompositionalLayout 实现弹性头部(Stretchy Header)方案
核心问题分析
你当前实现无效的原因:
- 直接继承
UICollectionViewCompositionalLayout重写layoutAttributesForElements时,组合布局的内部属性生成逻辑与传统流布局不同,无法直接通过遍历拿到可修改的头部布局属性。 - 未给头部设置固定置顶的布局规则,即使修改属性也无法保持头部在顶部。
以下是从零开始的完整实现步骤:
步骤1:实现弹性头部视图
调整内部图片的约束,确保拉伸时能正确填充显示:
class StretchyCollectionHeaderView: UICollectionReusableView { static let reuseIdentifier = "stretchyCollectionHeaderView-reuse-identifier" let imageView: UIImageView = { let iv = UIImageView(image: UIImage(named: "HeaderHomePage")) iv.contentMode = .scaleAspectFill iv.clipsToBounds = true // 裁剪超出底部的部分,顶部允许超出以实现拉伸 return iv }() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .red addSubview(imageView) // 设置约束:图片底部与头部底部对齐,顶部比头部顶部多延伸200pt(预留拉伸空间) imageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ imageView.leadingAnchor.constraint(equalTo: leadingAnchor), imageView.trailingAnchor.constraint(equalTo: trailingAnchor), imageView.bottomAnchor.constraint(equalTo: bottomAnchor), imageView.topAnchor.constraint(equalTo: topAnchor, constant: -200) ]) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 监听头部frame变化,动态调整图片位置实现拉伸效果 override func layoutSubviews() { super.layoutSubviews() let offset = frame.minY imageView.frame = CGRect(x: 0, y: offset, width: frame.width, height: frame.height - offset) } }
步骤2:创建支持弹性头部的自定义组合布局
关键逻辑:
- 重写
layoutAttributesForSupplementaryView(ofKind:at:)直接修改头部布局属性,处理拉伸与置顶逻辑 - 重写
shouldInvalidateLayout(forBoundsChange:)确保滚动时实时更新布局
class StretchyHeaderLayout: UICollectionViewCompositionalLayout { // 头部原始高度(根据需求调整) private let headerOriginalHeight: CGFloat = 200 override init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider) { super.init(sectionProvider: sectionProvider) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) else { return nil } // 仅处理第一个分区的头部 guard indexPath.section == 0, elementKind == UICollectionView.elementKindSectionHeader else { return attributes } guard let collectionView = collectionView else { return attributes } let contentOffsetY = collectionView.contentOffset.y // 向下滚动(contentOffsetY为负)时拉伸头部 if contentOffsetY < 0 { let stretchHeight = headerOriginalHeight - contentOffsetY attributes.frame = CGRect( x: 0, y: contentOffsetY, width: collectionView.frame.width, height: stretchHeight ) } else { // 向上滚动时保持头部固定在顶部 attributes.frame = CGRect( x: 0, y: 0, width: collectionView.frame.width, height: headerOriginalHeight ) } return attributes } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { // 滚动时实时更新布局 return true } }
步骤3:生成布局时配置头部置顶规则
在第一个分区的布局中添加边界补充视图,并设置pinToVisibleBounds = true实现置顶:
func generateLayout() -> UICollectionViewLayout { let layout = StretchyHeaderLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in let isWideView = layoutEnvironment.traitCollection.horizontalSizeClass == .regular let sectionLayoutKind = Section.allCases[sectionIndex] switch sectionLayoutKind { case .locationTab: // 生成第一个分区的基础布局 let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16) // 配置弹性头部 let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(200)) let header = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top ) header.pinToVisibleBounds = true // 固定头部在可见区域顶部 header.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0) section.boundarySupplementaryItems = [header] return section case .selectCategory: return self.generateCategoriesLayout() case .valueAddedServices: return self.generatValueAddedServicesLayout(isWide: isWideView) } } return layout }
步骤4:配置数据源补充视图提供者
保留你原有的代码即可,确保第一个分区使用自定义弹性头部:
dataSource.supplementaryViewProvider = { ( collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in if indexPath.section == 0 { guard let supplementaryView = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier, for: indexPath) as? StretchyCollectionHeaderView else { fatalError("Cannot create header view") } supplementaryView.imageView.image = UIImage(named: "HeaderHomePage") return supplementaryView } else { guard let supplementaryView = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView else { fatalError("Cannot create header view") } supplementaryView.label.text = Section.allCases[indexPath.section].rawValue return supplementaryView } }
额外注意事项
- 务必在
viewDidLoad中注册头部视图:
collectionView.register(StretchyCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier) collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.reuseIdentifier)
- 若需要全局头部(不绑定任何分区),可直接通过
layout.boundarySupplementaryItems添加,无需绑定到单个分区。
内容的提问来源于stack exchange,提问作者Mel
相关产品推荐
相关产品推荐

