观察SwiftUI ScrollView的内容偏移量

本文探讨了在SwiftUI中使用UIScrollView时遇到的问题,介绍了如何利用GeometryReader和PositionObservingView创建一个可以观察内容偏移的自定义ScrollView。作者详细解释了如何使用SwiftUI的偏好系统来处理滚动视图的滚动位置,并提供了一个完整的示例应用.
摘要由CSDN通过智能技术生成

观察SwiftUI ScrollView的内容偏移量

在构建各种可滚动UI时,通常希望观察当前滚动位置(UIScrollView称之为内容偏移),以触发布局更改,在需要时加载其他数据,或根据用户当前正在查看的内容执行其他类型的操作。

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

解决这个问题的一种方法是利用UIKit的UIScrollView的丰富功能,由于其代理协议和scrollViewDidScroll方法,它提供了一种在发生任何类型的滚动时获得通知的简单方法。然而,尽管我通常非常喜欢使用UIViewRepresentable和其他 SwiftUI/UIKit互操作性机制,但在这种情况下,我们必须编写相当多的额外代码来弥合两个框架之间的差距。

这主要是因为——至少在iOS上——我们只能将SwiftUI内容嵌入到UIHostingController中,而不是在自我管理的UIView中。因此,如果我们想使用UIScrollView构建一个自定义的、可观察的ScrollView版本,那么我们必须将该实现包装在视图控制器中,然后管理我们的UIHostingController与键盘、滚动视图的内容大小、安全区域嵌入等之间的关系。虽然不是不可能,但仍然有相当多的额外工作和复杂性。

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

使用GeometryReader解析框架

在开始之前,要意识到的关键一点是,UIScrollView和SwiftUI的ScrollView都通过修改一个容器偏移执行滚动,该容器托管实际可滚动内容。然后,他们将该容器裁剪到边界上,以产生视口移动的错觉。因此,如果能找到观察该容器框架(frame)的方法,那么基本上也会找到观察滚动视图内容偏移的方法。

这就是我们的老朋友GeometryReader发挥作用的地方(没有它,就不会是一个合适的SwiftUI布局解决方法,对吗?)。虽然GeometryReader主要用于访问它所托管的视图的size大小(或者更准确地说,该视图的拟议大小),但它还有另一个巧妙的技巧——可以要求它读取相对于给定坐标系的当前视图的frame框架。

要使用该功能,让我们从创建一个PositionObservingView开始,它允许将CGPoint值绑定到该视图相对于 CoordinateSpace的当前位置,我们也将将其作为参数传递。然后,新视图将嵌入一个GeometryReader作为背景(这将使该GeometryReader具有与视图本身相同的大小),并将使用首选项键将已经解析的框架的origin原点设置为偏移量——像这样:

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
            }
    }
}

要了解如何使用@ViewBuilder属性构建自定义SwiftUI容器视图的更多信息,请查看这篇文章

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

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

private extension PositionObservingView {
    struct PreferenceKey: SwiftUI.PreferenceKey {
        static var defaultValue: CGPoint { .zero }

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

实际上不需要实现上述任何类型的reduce算法,因为只有单个视图在任何给定的层次结构中使用该首选项键的传递值(因为上述实现完全包含在PositionObservingView中)。

好吧,现在有了一个能够读取和观察自己在给定坐标系中位置的视图。让我们使用该视图构建一个ScrollView包装器, 实现我们的原始目标——能够在此类滚动视图中读取当前内容偏移量。

从位置到内容偏移

新的ScrollView包装器基本上将有两个职责——一,它需要将内部PositionObservingView的位置转换为当前滚动位置(或内容偏移),二,它还需要定义一个CordinateSpace坐标空间,内部视图可以使用它来解析其位置。除此之外,它只需将其配置参数转发到其底层的ScrollView, 以决定滚动视图在哪些轴上运行,以及是否显示滚动指示器。

好消息是,将内部视图的位置转换为内容偏移就像给CGPoint的x和y分量取负一样简单。这是因为,如前所述,滚动视图的内容偏移量本质上只是容器相对于滚动视图边界移动的距离。

因此,继续实施我们的自定义滚动视图,将其命名为OffsetObservingScrollView(这种情况下拼写出ContentOffset确实有点太冗长了):

struct OffsetObservingScrollView<Content: View>: View {
    var axes: Axis.Set = [.vertical]
    var showsIndicators = true
    @Binding var offset: CGPoint
    @ViewBuilder var content: () -> Content

    // The name of our coordinate space doesn’t have to be
    // stable between view updates (it just needs to be
    // consistent within this view), so we’ll simply use a
    // plain UUID for it:
    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)
    }
}

注意我们如何通过使用闭包定义getter和setter,为内部视图的位置参数创建完全自定义的绑定。如上述情况,在将一个值分配给另一个绑定之前需进行转换时,自定义的绑定是一个很好的选择。

就是这样!现在有了一个SwiftUI内置ScrollView的临时替代品,它能够观察当前内容偏移量——可以将其绑定到任何状态属性,例如更改头部视图的布局,向服务器报告分析事件,或执行任何其他类型的基于滚动位置的操作。你可以在这里找到一个使用上述OffsetObservingScrollView的完整示例,以实现可折叠的头部视图。

我希望你发现这篇文章有趣且有用。如果有任何问题、评论或反馈,请随时在Mastodon 上找到我,或通过电子邮件与我联系。

感谢您的阅读!

【课程特点】1、231节大容量课程:包含了SwiftUI的大部分知识点,详细讲解SwiftUI的方方面面;2、15个超级精彩的实例:包含美食、理财、健身、教育、电子商务等各行业的App实例;3、创新的教学模式:手把手教您SwiftUI用户界面开发技术,一看就懂,一学就会;4、贴心的操作提示:让您的眼睛始终处于操作的焦点位置,不用再满屏找光标;5、语言简洁精练:瞄准问题的核心所在,减少对思维的干扰,并节省您宝贵的时间;6、视频短小精悍:即方便于您的学习和记忆,也方便日后对功能的检索;7、齐全的学习资料:提供所有课程的源码,在Xcode 11 + iOS 13环境下测试通过; 更好的应用,更少的代码!SwiftUI是苹果主推的下一代用户界面搭建技术,具有声明式语法、实时生成界面预览等特性,可以为苹果手机、苹果平板、苹果电脑、苹果电视、苹果手表五个平台搭建统一的用户界面。SwiftUI是一种创新、简单的iOS开发中的界面布局方案,可以通过Swift语言的强大功能,在所有的Apple平台上快速构建用户界面。 仅使用一组工具和API为任何Apple设备构建用户界面。SwiftUI具有易于阅读和自然编写的声明式Swift语法,可与新的Xcode设计工具无缝协作,使您的代码和设计**同步。自动支持动态类型、暗黑模式、本地化和可访问性,意味着您的**行SwiftUI代码已经是您编写过的非常强大的UI代码了。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值