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

如何固定UICollectionViewCompositionalLayout的整个第一分区?

实现固定整个第一分区在CollectionView顶部的方案

针对你的需求,将第一分区的完整内容嵌入到Section Header中,利用pinToVisibleBounds属性固定头部是最可靠的系统原生方案,具体实现步骤如下:

核心思路

把原本的第一分区内容,封装成一个自定义的Section Header视图(内部嵌入横向滚动的UICollectionView),然后给第一个实际业务分区(原第二分区)设置这个Header并开启固定,以此模拟整个分区固定在顶部的效果。

具体实现代码

1. 创建固定头部的自定义视图

这个视图内部包含一个横向滚动的CollectionView,复用你原本第一分区的布局逻辑:

class StickyFullSectionHeader: UICollectionReusableView {
    static let reuseID = "StickyFullSectionHeader"
    
    lazy var innerCollectionView: UICollectionView = {
        // 复用原第一分区的横向布局
        let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(120), heightDimension: .absolute(90))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(120), heightDimension: .absolute(90))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        section.interGroupSpacing = 16
        section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.showsHorizontalScrollIndicator = false
        return cv
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(innerCollectionView)
        NSLayoutConstraint.activate([
            innerCollectionView.topAnchor.constraint(equalTo: topAnchor),
            innerCollectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
            innerCollectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
            innerCollectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

2. 配置主CollectionView的CompositionalLayout

调整分区结构,给第一个业务分区(原第二分区)添加固定头部:

func createMainLayout() -> UICollectionViewCompositionalLayout {
    return UICollectionViewCompositionalLayout { sectionIndex, _ in
        // 原第二至第六分区的布局逻辑(现在对应sectionIndex 0-4)
        let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(120), heightDimension: .absolute(90))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(120), heightDimension: .absolute(90))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        section.interGroupSpacing = 16
        section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
        
        // 给第一个业务分区添加固定头部(原第一分区内容)
        if sectionIndex == 0 {
            let headerSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(106) // 对应头部视图的实际高度
            )
            let header = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerSize,
                elementKind: UICollectionView.elementKindSectionHeader,
                alignment: .top
            )
            header.pinToVisibleBounds = true // 开启固定
            section.boundarySupplementaryItems = [header]
        }
        
        return section
    }
}

3. 主CollectionView数据源配置

注册头部视图,并处理头部内部CollectionView的数据源:

// 注册头部视图
collectionView.register(
    StickyFullSectionHeader.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: StickyFullSectionHeader.reuseID
)

// 数据源方法:返回头部视图
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    guard kind == UICollectionView.elementKindSectionHeader, indexPath.section == 0 else {
        return UICollectionReusableView()
    }
    let header = collectionView.dequeueReusableSupplementaryView(
        ofKind: kind,
        withReuseIdentifier: StickyFullSectionHeader.reuseID,
        for: indexPath
    ) as! StickyFullSectionHeader
    
    // 配置头部内部CollectionView的数据源和代理(复用原第一分区的逻辑)
    header.innerCollectionView.dataSource = self
    header.innerCollectionView.delegate = self
    header.innerCollectionView.reloadData()
    
    return header
}

// 区分主CollectionView和头部内部CollectionView的数据源方法
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    if collectionView === (self.collectionView.visibleSupplementaryViews(ofKind: UICollectionView.elementKindSectionHeader).first as? StickyFullSectionHeader)?.innerCollectionView {
        // 返回原第一分区的item数量
        return 8
    } else {
        // 返回对应业务分区的item数量
        return 6
    }
}

方案说明

  • 优点:完全利用系统原生的pinToVisibleBounds机制,滚动性能稳定,不会出现布局错乱或卡顿问题。
  • 注意事项:需要调整原有的分区索引逻辑(原第一分区的内容移到头部,业务分区从index 0开始),同时要区分主CollectionView和头部内部CollectionView的数据源/代理逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 10:12:10