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

如何检测UITableView分区头部停靠/取消停靠并切换自定义视图?

刚好做过类似的需求!苹果确实没有提供一个专门的回调来通知section header进入/退出粘顶状态,但我们可以借助UITableView继承自UIScrollView的特性,通过scrollViewDidScroll代理方法来实时检测头部的位置,从而实现视图切换。

核心思路

  1. 在滚动过程中,获取目标分区头部的frame,并转换为屏幕坐标系下的位置。
  2. 判断该头部的顶部是否已经到达屏幕的顶部(需考虑导航栏、安全区域的高度),以此确定是否处于粘顶状态。
  3. 当粘顶状态发生变化时,切换自定义section header内部的子视图(避免重复创建视图,提升性能)。

具体实现步骤

1. 自定义Section Header视图

先创建一个包含两种状态的自定义header,内部集成SegmentedControl和倒计时视图,通过方法切换显示状态:

import UIKit

class CustomSectionHeader: UITableViewHeaderFooterView {
    // 初始化SegmentedControl
    private let segmentedControl: UISegmentedControl = {
        let sc = UISegmentedControl(items: ["Tab 1", "Tab 2"])
        sc.selectedSegmentIndex = 0
        sc.translatesAutoresizingMaskIntoConstraints = false
        return sc
    }()
    
    // 初始化倒计时视图(这里用示例视图,你可以替换成自己的实现)
    private let countdownView: UIView = {
        let view = UIView()
        view.backgroundColor = .orange
        let label = UILabel()
        label.text = "10D|4H|3M|03S"
        label.textColor = .white
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        view.translatesAutoresizingMaskIntoConstraints = false
        view.isHidden = true // 默认隐藏倒计时视图
        return view
    }()
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        setupLayout()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayout()
    }
    
    private func setupLayout() {
        // 将两个子视图添加到contentView并设置铺满约束
        contentView.addSubview(segmentedControl)
        contentView.addSubview(countdownView)
        
        NSLayoutConstraint.activate([
            segmentedControl.topAnchor.constraint(equalTo: contentView.topAnchor),
            segmentedControl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            segmentedControl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            segmentedControl.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            
            countdownView.topAnchor.constraint(equalTo: contentView.topAnchor),
            countdownView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            countdownView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            countdownView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }
    
    // 对外暴露切换视图的方法
    func toggleHeader(isStuck: Bool) {
        segmentedControl.isHidden = isStuck
        countdownView.isHidden = !isStuck
    }
}

2. 在ViewController中实现检测逻辑

首先注册自定义header,然后在scrollViewDidScroll中实时判断粘顶状态:

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    private let tableView = UITableView()
    private var currentHeaderIsStuck = false
    private let targetSection = 0 // 你要监控的分区索引
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    private func setupTableView() {
        tableView.frame = view.bounds
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.delegate = self
        tableView.dataSource = self
        
        // 注册自定义section header
        tableView.register(CustomSectionHeader.self, forHeaderFooterViewReuseIdentifier: "CustomSectionHeader")
        
        // 设置tableHeaderView(你的Banner GIF视图)
        let bannerView = UIView()
        bannerView.backgroundColor = .black
        bannerView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 200)
        tableView.tableHeaderView = bannerView
        
        view.addSubview(tableView)
    }
    
    // MARK: - UITableView DataSource
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 20
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
        cell.textLabel?.text = "Image \(indexPath.row + 1)"
        return cell
    }
    
    // MARK: - UITableView Delegate
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CustomSectionHeader") as? CustomSectionHeader else {
            return nil
        }
        // 初始化时设置正确的显示状态
        headerView.toggleHeader(isStuck: currentHeaderIsStuck)
        return headerView
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 50
    }
    
    // MARK: - UIScrollView Delegate
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // 获取目标分区头部的frame
        guard let headerRect = tableView.rectForHeader(inSection: targetSection) as CGRect? else { return }
        
        // 转换为屏幕坐标系下的位置
        let screenRect = tableView.convert(headerRect, to: nil)
        
        // 计算顶部参考高度(导航栏高度 + 安全区域顶部高度)
        let topReferenceHeight = (navigationController?.navigationBar.frame.height ?? 0) + view.safeAreaInsets.top
        
        // 判断是否处于粘顶状态:头部顶部 <= 参考高度
        let isStuck = screenRect.top <= topReferenceHeight
        
        // 只有状态变化时才更新视图,避免频繁触发
        if isStuck != currentHeaderIsStuck {
            currentHeaderIsStuck = isStuck
            // 更新header视图
            if let headerView = tableView.headerView(forSection: targetSection) as? CustomSectionHeader {
                headerView.toggleHeader(isStuck: isStuck)
            }
        }
    }
}

关键注意事项

  • 状态防抖:一定要通过currentHeaderIsStuck变量记录当前状态,只在状态变化时更新视图,否则scrollViewDidScroll每秒会触发几十次,导致视图闪烁。
  • 适配导航栏/安全区域:计算顶部参考高度时必须包含导航栏和安全区域的高度,否则在不同设备(比如刘海屏)上判断会不准确。
  • 视图复用:不要在scrollViewDidScroll中重新创建header视图,而是在自定义header内部切换子视图,这样既保证性能,也避免复用混乱。
  • 多分区场景:如果有多个分区,只需修改targetSection的值,或者循环判断所有分区的状态即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:47:21