如何让ScrollView的内容滚动,以与另一个视图的底边对齐?

移动开发 2026-07-12
var body: some View {
        ZStack(alignment: .top) {
            dateAndTime
            ScrollView {
                VStack {
                    storyContent
                }
            }

        }

在向下滚动时,如何让VStack的顶部边缘与dateAndTime标签的底部边缘对齐,而不是回弹回ScrollView的顶部?

起初,我希望VStack能隐藏dateAndTime,但我希望用户能够滚动并在停止滚动时显示dateAndTime标签,同时保持两者边缘对齐,不把滚动位置重置到顶部。

我尝试将dateAndTime放在ScrollView的背景中,并使用.scrollTargetLayout与 .scrollTargetBehavior,但似乎不起作用。我猜这是因为dateAndTime标签在ScrollView之外。我不确定如何使用scroll targets或锚点。

一个 ScrollView 与其背后的 dateAndTime 标签在堆叠中的画布预览

解决方案

我建议在视图被向下拉时,简单地在 VStack 顶部加一点内边距。这样就能让dateAndTime标签在 VStack 的背后可见。说明如下:

  • .onScrollGeometryChange 可以用来检测视图被向下拉动的时机。
  • 你也可能想要检查滚动阶段,这样只有在用户从静止状态与滚动视图交互时才显示头部,而不是在滚动大量内容重新进入视图时才显示。为此,可以使用 .onScrollPhaseChange
  • 有一个版本的 .onScrollPhaseChange 可以把 ScrollPhaseChangeContext 提供给闭包。这也使得在阶段变化时检查滚动偏移成为可能。然而,我发现它只在阶段变化的瞬间报告偏移量,而不是持续地。因此,.onScrollGeometryChange 也可能是必要的。
  • 如果你想在视图没有被向下拉时对dateAndTime进行遮罩,那么背景可能需要应用到 VStack
  • 如果内容较短(如你的示例),通过在底部添加大量负的底边距,背景就能延伸到屏幕底部。或者,如果已知屏幕的几何尺寸,可以计算背景的高度,但负边距也许更简单且更合适。

下面是一个更详细的示例,演示它全部工作:

struct ContentView: View {
    let headerHeight: CGFloat = 50
    let scrollThreshold: CGFloat = 10
    @State private var isInteractingFromRest = false
    @State private var isHeaderShowing = false

    private var dateAndTime: some View {
        // ...
    }

    private var storyContent: some View {
        // ...
    }

    var body: some View {
        NavigationStack {
            ZStack(alignment: .top) {
                dateAndTime
                ScrollView {
                    VStack {
                        storyContent
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .background {
                        Color.blue
                            .padding(.bottom, -2000)
                    }
                    .padding(.top, isHeaderShowing ? headerHeight : 0)
                }
                .onScrollPhaseChange { _, newPhase, context in
                    if newPhase == .interacting {
                        let geo = context.geometry
                        let scrollOffset = geo.contentOffset.y + geo.contentInsets.top
                        isInteractingFromRest = abs(scrollOffset) < 1
                    } else {
                        isInteractingFromRest = false
                    }
                }
                .onScrollGeometryChange(for: Bool.self) { geo in
                    let result: Bool
                    if isInteractingFromRest {
                        let scrollOffset = geo.contentOffset.y + geo.contentInsets.top
                        result = scrollOffset < (isHeaderShowing ? scrollThreshold : -scrollThreshold)
                    } else {
                        result = isHeaderShowing
                    }
                    return result
                } action: { _, newVal in
                    if newVal != isHeaderShowing {
                        withAnimation {
                            isHeaderShowing = newVal
                        }
                    }
                }
                .toolbar {
                    ToolbarItemGroup(placement: .topBarTrailing) {
                        Button("Edit") {}
                        Button("Delete", systemImage: "trash") {}
                    }
                }
            }
            .background(.red.opacity(0.5))
        }
    }
}

动画

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章