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

在ScrollView内的LazyVStack顶部实现多个固定头部视图(适配NavigationStack大标题动画)

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 16:33:13