如何检测UITableView分区头部停靠/取消停靠并切换自定义视图?
刚好做过类似的需求!苹果确实没有提供一个专门的回调来通知section header进入/退出粘顶状态,但我们可以借助UITableView继承自UIScrollView的特性,通过scrollViewDidScroll代理方法来实时检测头部的位置,从而实现视图切换。
核心思路
- 在滚动过程中,获取目标分区头部的frame,并转换为屏幕坐标系下的位置。
- 判断该头部的顶部是否已经到达屏幕的顶部(需考虑导航栏、安全区域的高度),以此确定是否处于粘顶状态。
- 当粘顶状态发生变化时,切换自定义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
相关产品推荐
相关产品推荐

