Scroll View内嵌Table View:滚动时展开Table View并隐藏上方视图
嘿,这个交互确实是iOS App里超常见的体验——像微信的顶部搜索栏、微博的导航栏,都是这么做的!我来给你拆解下具体的实现思路和代码示例,分UIKit(Swift/OC)和SwiftUI两种场景来说~
核心原理
核心就是监听TableView的滚动偏移量,对比上次的偏移值判断滚动方向,然后通过平滑动画调整顶部视图的位置:
- 向下滚动时,让顶部视图向上滑出屏幕(隐藏)
- 向上滚动时,让顶部视图重新滑入屏幕(显示)
因为TableView继承自UIScrollView,所以我们可以利用UIScrollViewDelegate的回调来实现这个逻辑。
UIKit 实现步骤(Swift)
假设你已经在Storyboard/XIB里搭建好UI:顶部有个topView,下方是tableView,并且给topView添加了顶部约束(比如和父视图/SafeArea顶部的约束,记为topViewTopConstraint),topView的高度固定为60。
1. 定义状态变量
在ViewController里添加几个变量记录滚动状态和视图参数:
import UIKit class ViewController: UIViewController { // 关联Storyboard里的控件 @IBOutlet weak var topView: UIView! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var topViewTopConstraint: NSLayoutConstraint! // 记录上次的滚动偏移量 private var lastContentOffset: CGFloat = 0 // 顶部视图的固定高度 private let topViewHeight: CGFloat = 60 override func viewDidLoad() { super.viewDidLoad() // 设置TableView的代理 tableView.delegate = self // 模拟一些列表数据 tableView.dataSource = self } }
2. 实现滚动监听逻辑
遵守UIScrollViewDelegate,实现scrollViewDidScroll方法:
extension ViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { // 先判断列表内容是否足够滚动,避免内容过短误触发 guard scrollView.contentSize.height > scrollView.bounds.height else { topViewTopConstraint.constant = 0 return } let currentOffset = scrollView.contentOffset.y let offsetDiff = currentOffset - lastContentOffset // 根据滚动方向调整顶部视图的约束 if offsetDiff > 0 { // 向下滚动:隐藏topView,约束值最小为 -topViewHeight(完全滑出屏幕) topViewTopConstraint.constant = max(topViewTopConstraint.constant - offsetDiff, -topViewHeight) } else { // 向上滚动:显示topView,约束值恢复到0(完全显示) topViewTopConstraint.constant = min(topViewTopConstraint.constant - offsetDiff, 0) } // 更新上次偏移量 lastContentOffset = currentOffset // 触发平滑动画 UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() } } } // 补充TableView数据源(示例) extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 50 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = "第 \(indexPath.row + 1) 行" return cell } }
UIKit 实现步骤(OC)
如果用Objective-C开发,逻辑是一样的:
1. 定义状态变量
#import "ViewController.h" @interface ViewController () <UITableViewDelegate, UITableViewDataSource> @property (weak, nonatomic) IBOutlet UIView *topView; @property (weak, nonatomic) IBOutlet UITableView *tableView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *topViewTopConstraint; @property (nonatomic, assign) CGFloat lastContentOffset; @property (nonatomic, assign) CGFloat topViewHeight; @end
2. 实现滚动监听
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.tableView.delegate = self; self.tableView.dataSource = self; self.topViewHeight = 60; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // 判断内容是否足够滚动 if (scrollView.contentSize.height <= scrollView.bounds.height) { self.topViewTopConstraint.constant = 0; return; } CGFloat currentOffset = scrollView.contentOffset.y; CGFloat offsetDiff = currentOffset - self.lastContentOffset; if (offsetDiff > 0) { // 向下滚动 self.topViewTopConstraint.constant = MAX(self.topViewTopConstraint.constant - offsetDiff, -self.topViewHeight); } else { // 向上滚动 self.topViewTopConstraint.constant = MIN(self.topViewTopConstraint.constant - offsetDiff, 0); } self.lastContentOffset = currentOffset; // 平滑动画 [UIView animateWithDuration:0.3 animations:^{ [self.view layoutIfNeeded]; }]; } // 数据源实现(示例) - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 50; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; cell.textLabel.text = [NSString stringWithFormat:@"第 %ld 行", indexPath.row + 1]; return cell; } @end
SwiftUI 实现思路
如果用SwiftUI开发,可以通过监听ScrollView的滚动偏移来控制顶部视图的位置:
import SwiftUI struct ScrollHideTopView: View { // 记录滚动偏移量 @State private var scrollOffset: CGFloat = 0 // 顶部视图高度 let topViewHeight: CGFloat = 60 var body: some View { ScrollView { VStack(spacing: 0) { // 顶部视图:根据滚动偏移调整位置 Color.blue .frame(height: topViewHeight) .offset(y: scrollOffset > 0 ? -min(scrollOffset, topViewHeight) : 0) .animation(.easeInOut(duration: 0.3), value: scrollOffset) // 列表内容 ForEach(0..<50) { index in Text("第 \(index + 1) 行") .frame(height: 50) .border(.gray) } } } // 监听滚动偏移 .background( GeometryReader { proxy in Color.clear .preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .global).origin.y) } ) .onPreferenceChange(ScrollOffsetKey.self) { offset in scrollOffset = -offset // 转换坐标系,让偏移值符合直觉 } } } // 自定义PreferenceKey获取滚动偏移 struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} } struct ScrollHideTopView_Previews: PreviewProvider { static var previews: some View { ScrollHideTopView() } }
优化小技巧
- 添加滚动阈值:比如判断
abs(offsetDiff) > 5才触发动画,避免轻微滚动就频繁调整视图位置 - 性能优化:如果顶部视图有复杂UI,可以设置
topView.layer.shouldRasterize = true,减少动画卡顿 - 边界处理:确保列表滚动到顶部时,顶部视图一定完全显示;滚动到底部时保持隐藏状态
内容的提问来源于stack exchange,提问作者Darius
相关产品推荐
相关产品推荐

