在ScrollView内的LazyVStack顶部实现多个固定头部视图(适配NavigationStack大标题动画)
嘿,这个需求我熟!要在ScrollView的LazyVStack顶部搞两个固定头部,还得让NavigationStack的大标题滚动动画正常工作,其实核心就是用SwiftUI的偏好键动态获取头部高度,再通过偏移和内边距的配合实现粘性效果,我给你写个完整的可运行示例,再拆解关键部分~
完整实现代码
import SwiftUI // 用来监听头部视图高度的偏好键 struct StickyHeaderHeightKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } } // 自定义头部视图,自动上报自身高度 struct StickyHeaderView<Content: View>: View { let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { content .background( GeometryReader { proxy in Color.clear.preference( key: StickyHeaderHeightKey.self, value: proxy.size.height ) } ) } } struct DoubleStickyHeaderDemo: View { // 模拟50条列表数据 private let items = Array(1...50) // 存储两个头部的总高度 @State private var totalHeaderHeight: CGFloat = 0 var body: some View { NavigationStack { ScrollView { LazyVStack(spacing: 0) { // 第一个固定头部 StickyHeaderView { Text("第一个固定头部") .frame(maxWidth: .infinity) .padding() .background(.blue) .foregroundColor(.white) } // 第二个固定头部 StickyHeaderView { Text("第二个固定头部") .frame(maxWidth: .infinity) .padding() .background(.orange) .foregroundColor(.white) } // 列表内容区域 ForEach(items, id: \.self) { item in Text("列表项 \(item)") .frame(maxWidth: .infinity) .padding() .background(.gray.opacity(0.1)) } } // 核心:让LazyVStack向上偏移头部总高度 .offset(y: -totalHeaderHeight) // 裁剪掉偏移后超出的部分 .clipped() // 给ScrollView顶部加对应内边距,露出头部 .padding(.top, totalHeaderHeight) } // 监听偏好键变化,更新总头部高度 .onPreferenceChange(StickyHeaderHeightKey.self) { newValue in totalHeaderHeight = newValue * 2 } .navigationTitle("双固定头部Demo") .navigationBarTitleDisplayMode(.large) } } } #Preview { DoubleStickyHeaderDemo() }
关键部分拆解
- 偏好键(PreferenceKey)的作用:我用
StickyHeaderHeightKey来动态获取每个头部的实际高度,毕竟不同设备、不同字体大小下头部高度可能不一样,硬写固定值太死板,这样能适配各种场景。 - 粘性效果的核心逻辑:通过给LazyVStack设置
.offset(y: -totalHeaderHeight)让整个列表向上偏移两个头部的总高度,再给ScrollView顶部加同样高度的.padding(.top, totalHeaderHeight),这样前两个头部就会刚好“卡”在ScrollView的顶部,滚动列表内容时头部不会跟着动,实现固定效果。 - 兼容大标题动画:整个实现完全在ScrollView内部完成,没有修改NavigationStack的默认行为,所以大标题的“从大变小”滚动动画会正常触发,和原生效果完全一致。
注意事项
- 如果两个头部高度不一样,你可以给每个头部单独定义偏好键(比如
FirstHeaderHeightKey和SecondHeaderHeightKey),然后totalHeaderHeight就是两个高度的和,这样更精准。 - 记得把LazyVStack的
spacing设为0,不然头部之间会有空隙,影响固定后的视觉效果。 - 一定要在真机或者模拟器上测试滚动效果,有时候预览窗口里的动画表现可能和真实设备有差异。
备注:内容来源于stack exchange,提问作者merkredez
相关产品推荐
相关产品推荐

