如何监听 SwiftUI ScrollView 的 contentOffset

当我们构建可滚动的视图时,会遇到一些很常见的需求。比如监听视图内容滚动的偏移量,去触发布局的更新、在合适的时机加载数据或者根据用户当前正在查看的内容执行其他类型的操作。

然而,在使用 SwiftUI 的 ScrollView 时,目前没有内置方法来对此类滚动进行监听。虽然在滚动视图中嵌入 ScrollViewReader 确实使我们能够更改代码中的滚动位置,但奇怪的是(特别是考虑到它的名称)不允许我们以任何方式读取当前内容偏移量。

解决这个问题的一种方法是需要使用 UIKitUIScrollView,通过实现 scrollViewDidScroll方法来获得任何类型的滚动发生时的通知。然而,如果在 SwiftUI 中使用 UIKit 的控件,在这种情况下,我们必须编写相当多的额外代码来弥合两个框架之间的差距。

这主要是因为我们只能在一个 UIHostingController 中嵌入 SwiftUI 内容,而不是在一个自我管理的 UIView 中 (至少在 iOS 上是如此的)。如果我们想用 UIScrollView 建立一个自定义的,可观察偏移量版本的 ScrollView ,那么我们需要把那个实现包装在一个视图控制器中,然后管理 UIHostingController和键盘,滚动视图的内容大小,安全区域插入等等之间的关系。无论如何也不是不可能,但是仍然有相当多的额外工作。

因此,让我们看看是否可以找到一种完全 SwiftUI 原生的方法来执行此类内容偏移观察。

GeometryReader

在我们开始之前需要意识到的一件重要的事情是:UIScrollViewSwiftUIScrollView 都是通过偏移一个承载我们实际可滚动内容的容器来执行滚动的。然后它们将容器夹在边界上,以产生移动的错觉。如果我们能找到一种方法来观察那个容器的框架,那么我们就能找到一种方法来观察滚动视图的内容偏移量。

这就需要 GeometryReader进来的地方出场了。虽然 GeometryReader 主要用于访问它所承载的视图的大小(或者更准确地说,视图的建议大小),但它还有另一个巧妙的技巧:可以使用它读取相对于给定坐标系的当前视图。

要使用这个功能,让我们从创建一个 PositionObservingView 开始,它允许我们将 CGPoint 值绑定到该视图相对于 CoordinateSpace 的当前位置,我们也将作为参数传递给它。然后,我们的新视图将嵌入一个几何阅读器作为背景(这将使几何阅读器与视图本身具有相同的大小),并将使用偏好键将解析帧的原点分配为我们的偏移量,代码如下:

swift 复制代码
struct PositionObservingView<Content: View>: View {
    var coordinateSpace: CoordinateSpace
@Binding var position: CGPoint
    @ViewBuilder var content: () -> Content

    var body: some View {
        content()
            .background(GeometryReader { geometry in
                Color.clear.preference(
    key: PreferenceKey.self,
    value: geometry.frame(in: coordinateSpace).origin
)
            })
            .onPreferenceChange(PreferenceKey.self) { position in
                self.position = position
            }
    }
}

我们在上面使用 SwiftUI 的偏好系统的原因是,我们的 GeometryReader 将作为视图更新过程的一部分被调用,并且我们不允许在该过程中直接改变视图的状态。因此,通过使用偏好系统,我们可以以异步方式将 CGPoint 值传递给视图,然后将这些值与 position 绑定。

现在我们需要做的就是实现上面使用的 PreferenceKey 类型,代码如下:

swift 复制代码
private extension PositionObservingView {
    struct PreferenceKey: SwiftUI.PreferenceKey {
        static var defaultValue: CGPoint { .zero }

        static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
    }
}

我们实际上不需要实现上面的 reduce 方法,因为我们只有一个视图在任何给定的层次结构中使用 preference key 传递值。

OK,现在我们有了一个视图它能够读取和观察自己在给定坐标系中的位置。现在让我们使用这个视图来构建一个 ScrollView 包装器,它将让我们实现最初的目标------能够在一个滚动视图中读取当前内容偏移量。

监听内容偏移量

让我们继续实现自定义滚动视图,我们将其命名为 OffsetObservingScrollView

less 复制代码
struct OffsetObservingScrollView<Content: View>: View {
    var axes: Axis.Set = [.vertical]
    var showsIndicators = true
    @Binding var offset: CGPoint
    @ViewBuilder var content: () -> Content
    private let coordinateSpaceName = UUID()

    var body: some View {
        ScrollView(axes, showsIndicators: showsIndicators) {
            PositionObservingView(
                coordinateSpace: .named(coordinateSpaceName),
                position: Binding(
                    get: { offset },
                    set: { newOffset in
                        offset = CGPoint(
    x: -newOffset.x,
    y: -newOffset.y
)
                    }
                ),
                content: content
            )
        }
        .coordinateSpace(name: coordinateSpaceName)
    }
}

现在,我们现在有了 SwiftUI 内置的 ScrollView的替代品,它使我们能够观察当前的内容偏移量。我们可以将其绑定到任何我们想要的状态属性,例如,为了改变标题视图的布局,向我们的服务器报告分析事件,或者执行任何其他类型的基于滚动位置的操作。

相关推荐
I烟雨云渊T2 小时前
iOS swiftUI的实用举例
ios·swiftui·swift
大熊猫侯佩3 小时前
SwiftUI 中为何 DisclosureGroup 视图在收缩时没有动画效果?
swiftui·swift·apple
Fatbobman(东坡肘子)2 天前
WWDC 2025 开发者特辑 | 肘子的 Swift 周报 #088
开发语言·macos·ios·swiftui·ai编程·swift·wwdc
大熊猫侯佩2 天前
SwiftUI 6.0(iOS 18)新容器视图修改器漫谈
ios·swiftui·wwdc
SoaringHeart3 天前
SwiftUI组件封装:仿 Flutter 原生组件 Wrap实现
ios·swiftui
YungFan3 天前
SwiftUI-自定义与扩展
swiftui·swift
东坡肘子3 天前
WWDC 2025 开发者特辑 | 肘子的 Swift 周报 #088
swiftui·swift·wwdc