欢迎回来,这一节,我们实现 ScrollView 下拉刷新的功能。

RefreshableScrollView 的属性

先来看相关属性的部分:

public struct RefreshableScrollView<Content: View>: View {
  @State private var previousScrollOffset: CGFloat = 0
  @State private var scrollOffset: CGFloat = 0

  // Keep the loading indication area above the scroll view.
  @State private var frozen: Bool = false
  @State private var rotation: Angle = .degrees(0)

  // Trigger the action after scrolling over the threshold.
  var threshold: CGFloat = 80
  let content: Content

  // Pull down to refresh
  @Binding var refreshing: Bool
}

可以看到,RefreshableScrollView 也属于一个“高阶 UI 容器”。它有一个泛型参数 Content 表示要在滚动视图中显示的内容。在它的诸多属性中:

  • previousScrollOffset 可以理解成是 RefreshableScrollView 在两次滚动事件之间,上一次滚动位置的偏移;
  • scrollOffsetRefreshableScrollView 当前的滚动位置偏移;至于为什么要区分这两个位置,我们稍后再说;
  • frozenrotation 是要传给上一节创建的 SymbolView 的值,用于“冻”住它的位置,并设置下拉图标的旋转角度;

以上这四个值,都是 RefreshableScrollView 自身管理的状态,不同的 RefreshableScrollView 对象,应该有它们自己的状态值,因此它们都定义成了 @State。而 refreshing 则用于控制 RefreshableScrollView 当前是否在保持刷新数据的状态,这个状态的完成,依赖于外部我们请求数据的过程,它不应该属于 RefreshableScrollView 自身管理的状态,而只应该知道如何获取它的内容,因此,我们把它定义成了 @Binding

最后,threshold 表示下拉超过多长时,触发更新机制。content 是要显示的滚动内容。至于 RefreshableScrollView 的其它属性,和下拉刷新就没关系了。它们属于上拉刷新的部分,因此等稍后我们实现的时候再说。

init 方法里,要做的也只是逐个对这些属性设置初始值而已,并没有做其它的操作。由于其中还包含着对上拉刷新部分的初始化,这里我们就暂时先不列出代码了。

RefreshableScrollView 的外框

然后,我们来实现 RefreshableScrollViewbody。由外向内,先来看它的“外边框”:

public var body: some View {
  ScrollView {
    ZStack(alignment: .top) {
      /// Scrolling content here.
    }
  }
  .backgroundPreferenceValue(/* PreferenceKey */) {
    /// Place a `FixedView` as background.
  }
}

可以看到,RefreshableScrollView 是基于 ScrollView 实现的。在 ScrollView 后面,我们用 backgroundPreferenceValue 设置了它的背景,这个背景实际上就是之前实现的 FixedView。当然,我们的目的并不是真的要设置背景,而是这样,就可以间接获取到 ScrollView 的实际尺寸了。

至于为什么要使用 backgroundPreferenceValue,其实它和下拉刷新没关系,而是和上拉刷新相关。所以我也没有提到它使用的具体的 key。现在,只要知道,这里我们通过把背景设置成 FixedView 获取 ScrollView 的尺寸就好了。

ScrollView 内部,我们摆了一个顶部对齐的 ZStack。这个 ZStack 内部,就应该是要滚动的内容了。

RefreshableScrollView 的内容

嗯,没错,应该是~

为什么是应该是呢?因为实际的实现要比想象的复杂一些 :) 我们对着代码来看:

ZStack(alignment: .top) {
  MovingView()

  VStack {
    self.content
  }
  .alignmentGuide(.top, computeValue: { d in
      (self.refreshing && self.frozen ? -self.threshold : 0.0) })

  SymbolView(height: self.threshold,
    loading: self.refreshing,
    frozen: self.frozen,
    rotation: self.rotation)
}

可以看到,滚动内容的顶部,是我们之前说过的 MovingView,因此,它就可以随着内容的滚动始终保持在滚动内容的顶端,这样一来,通过它,就可以得到当前内容的滚动位置了。

接下来,是一个 VStack,这里,才是真正要滚动显示的内容。值得说一下的,是我们设置它的位置的方法。alignmentGuide 除了支持用数值指定到 .top 的距离之外,它还有一个 computeValue 版本,允许我们指定一个计算方法。这个 closure 有一个 ViewDimensions 类型的参数,通过它可以在 View 自己的坐标系里,得到 View 的尺寸。不过这里,我们没有用到这个值。我们实现的逻辑是,只要 RefreshableScrollView 正在刷新,并且要“冻”住它的位置,就把它的位置设置成 -self.threshold,也就是开始触发数据刷新的位置,否则,它距离顶部,就是 0。

看到这里,你可能会觉得,“冻”住的位置为什么是 -self.threshold,如果 y 轴向下为正,应该是 self.threshold 才对啊。emm,应该说,这的确是 SwiftUI 中一个对于初学者而言,比较反直觉的东西。简单来说,alignmentGuide 的意思,是把参数指定的数值,对齐到指定的位置,只有把 -self.threshold,对齐到 .top 0 这个位置,才是我们想要的效果。接下来,如果有机会,我们会专门和大家说说 SwiftUI 中各种形式的对齐有关的内容,现在大家知道它的含义就好了。

滚动视图的 ZStack 里,最后一个内容,是显示下拉和加载提示图标的 SymbolView,我们就不重复说了。

因此,在 RefreshableScrollView 的滚动内容里,实际上叠了三层,分别是:MovingView,表示滚动内容的 VStackSymbolView

What's next?

至此,View 布局的部分就已经完成了。现在,稍微休息一下,整理下思路。下一节,我们完成随着下拉动态计算下拉距离,并触发更新机制的功能。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们