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

如何实现UICollectionView顶部固定1个header、底部固定1个header仅中间内容滚动

问题描述

我在UICollectionView中设置了多个section header,已经通过系统自带特性实现了第一个header固定在顶部的效果,现在需要实现第二个header固定在UICollectionView底部,其余header都排布在底部固定header的下方,仅两个固定header之间的内容可以滚动,也欢迎其他实现思路。
可见和不可见区域的视图控制器示意图

现有实现代码

顶部吸顶实现

if let layout = groupCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
        layout.sectionHeadersPinToVisibleBounds = true
    }

业务逻辑代码

extension GrpLVController: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout
{
 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
    return CGSize(width: collectionView.frame.width/3, height: 200)
}

func numberOfSections(in collectionView: UICollectionView) -> Int
{
    return totalSec.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
{
    return CGSize(width: self.view.frame.size.width, height: 36)
}

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
    switch kind {
    case UICollectionView.elementKindSectionHeader:
        let  headerV: GCell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "GCell", for: indexPath) as! GCell
        
        headerV.headerBackGroundView.layer.borderWidth = 0.5
        headerV.headerBackGroundView.layer.borderColor = UIColor.lightGray.cgColor
        
        let plusBtn = headerV.viewWithTag(101) as? UIButton
        if self.totalSec[indexPath.section] == selectedIndx
        {
            plusBtn?.setImage(UIImage(named: "Close"), for: .normal)
        }
        else
        {
            plusBtn?.setImage(UIImage(named: "Open"), for: .normal)
        }
        
        headerV.hDelegate = self
        
        headerV.headerBtnOut.tag = indexPath.section
        
        let sectionn = self.totalSec[indexPath.section]
        
        let lbl = headerV.viewWithTag(100) as? UILabel
        if sectionn == .Grp_Sugestion
        {
            lbl?.text = "Group suggestions"
        }
        else if sectionn == .MyGrp
        {
            lbl?.text = "My groups  (\(myGList.count))"
        }
        else if sectionn == .InvitesReceivedCorrect
        {
            lbl?.text = "Invites Received  (\(inviteList.count))"
        }
        else
        {
            lbl?.text = "Requests Sent  (\(requestedList.count))"
        }
        return headerV
    default:
        return UICollectionReusableView()
    }
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
    let sectionn = self.totalSec[section]
    if sectionn == .Grp_Suggestion && selectedIndx == .Grp_Suggestion
    {
        if suggestedList.count % 3 == 0
        {
            return suggestedList.count
        }
        else
        {
            return suggestedList.count - (suggestedList.count % 3) + 3
        }
    }
    else if sectionn == .MyGrp && selectedIndx == .MyGrp
    {
        if myGList.count % 3 == 0
        {
            return myGList.count
        }
        else
        {
            return myGList.count - (myGList.count % 3) + 3
        }
    }
    else if sectionn == .InvitesCorrect && selectedIndx == .InvitesCorrect
    {
        if inviteReveivedList.count % 3 == 0
        {
            return inviteList.count
        }
        else
        {
            return inviteList.count - (inviteList.count % 3) + 3
        }
    }
    else if sectionn == .InvitesReceived && selectedIndx == .InvitesReceived
    {
        if requestedList.count % 3 == 0
        {
            return requestedList.count
        }
        else
        {
            return requestedList.count - (requestedList.count % 3) + 3
        }
    }
    else
    {
        return 0
    }
}


func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GroupGridCell", for: indexPath) as! GroupGridCell
    cell.headerDelegate = self
    
    let sectionn = self.totalSec[indexPath.section]
    
    if sectionn == .Grp_Suggestion
    {
        if suggestedList.count > indexPath.row
        {
            cell.conf_G_InfoCell(group: suggestedList[indexPath.row])
        }
        else
        {
            cell.contentSubView.isHidden = true
        }
    }
    else if sectionn == .MyGroup
    {
        if myGList.count > indexPath.row
        {
            cell.configureGroupCell(group: myGList[indexPath.row])
        }
        else
        {
            cell.contentSubView.isHidden = true
        }
    }
    else if sectionn == .InvitesRCorrect
    {
        if inviteList.count > indexPath.row
        {
            cell.configureInviteCell(invite: inviteList[indexPath.row])
        }
        else
        {
            cell.contentSubView.isHidden = true
        }
    }
    else
    {
        if requestedList.count > indexPath.row
        {
            cell.configureInviteCell(invite: requestedList[indexPath.row])
        }
        else
        {
            cell.contentSubView.isHidden = true
        }
    }
    
    cell.contentSubView.layer.cornerRadius = 7.0
    cell.contentSubView.layer.masksToBounds = true
    cell.contentSubView.layer.borderWidth = 0.5
    cell.contentSubView.layer.borderColor = UIColor.lightGray.cgColor

    cell.RJoinBtn.layer.borderWidth = 1.0
    cell.RJoinBtn.layer.borderColor = UIColor.systemGreen.cgColor
    cell.RJoinBtn.layer.cornerRadius = 12.0
    cell.RJoinBtn.layer.masksToBounds = true

    cell.ivGPic.layer.borderWidth = 1.0
    cell.ivGPic.layer.borderColor = UIColor.white.cgColor
    cell.ivGPic.layer.cornerRadius =  8
    cell.ivGPic.layer.masksToBounds = true
    
    return cell
}
}

实现方案

方案1:自定义UICollectionViewFlowLayout(纯布局实现)

  • 重写layoutAttributesForElements(in rect: CGRect)方法,手动计算需要固定在底部的header的frame,将其zIndex设置为高于普通cell,避免被滚动内容遮挡
  • 重写shouldInvalidateLayout(forBoundsChange newBounds: CGRect)返回true,让滚动时布局实时更新
  • 核心逻辑:当底部header对应的section内容滚动到超过底部固定位置时,保持header的y值为collectionView.bounds.height - header高度 - collectionView.contentInset.bottom,如果内容还没到这个位置,就正常按照流布局排布

方案2:分层视图实现(改动最小,兼容性最好)

  • 把要固定在底部的header单独作为独立UIView,直接添加到UICollectionView的父视图上,位置固定在底部
  • 调整UICollectionView的contentInset,底部增加和固定header高度相等的内边距,避免底部滚动内容被固定header遮挡
  • 原有数据源中移除该固定header对应的section,剩余的section全部放在中间滚动区域,顶部吸顶效果保留原有的sectionHeadersPinToVisibleBounds配置即可
  • 底部固定header的点击交互直接给独立view添加事件即可,不需要和collectionView的补充视图逻辑绑定

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.10 03:48:01