这一节开始,我们来分享如何基于 SwftUI 实现支持下拉和上拉刷新的滚动视图。这是个在泊学 App 很多界面上都会用到的功能。由于它的实现相对独立,我们为这个 UI 组件单独创建了一个开源项目:BxRefreshableScrollView

应该说,这个实现方式多多少少有点 hack 的味道,它的实现思路是这样的。由于无法直接通过 ScrollView 获取滚动位置的数据,我们在 SwiftUI 的 ScrollView 上,覆盖了两个 View:一个高度为 0,它随着 ScrollView 的滚动而滚动,我们叫它 MovingView;另一个高度和 ScrollView 自身相同,并且位置固定,我们叫它 FixedView

这样,当滚动的时候,我们只要计算 MovingViewFixedView 顶部之间的距离,当它超过某个值时,就在顶部开始显示某个 View 并触发一个动作。就实现下拉刷新的效果了。

虽然道理听着挺简单,实现它还是稍微有一点点麻烦。而用到的主要技术,就是之前我们提到过的 PreferenceKeyPreferenceData

PreferenceKey 和 PreferenceData

因此,我们就从这两个类型说起。它们定义在 RefreshableKey.swift 里:

struct RefreshableKey {
  enum ViewType: Int {
    case movingView
    case fixedView
    case contentView
  }

  struct PrefData: Equatable {
    let vType: ViewType
    var bounds: CGRect
  }

  struct PrefKey: PreferenceKey {
    typealias Value = [PrefData]
    static var defaultValue: [PrefData] = []

    static func reduce(
      value: inout [RefreshableKey.PrefData],
      nextValue: () -> [RefreshableKey.PrefData]) {
      value.append(contentsOf: nextValue())
    }
  }
}

其中,PrefData 是在渲染滚动视图时,我们要添加在 View 上的额外数据。它有两个属性:

  • vType 用于标记数据的类型。就像 ViewType 中定义的,它分三种情况。movingViewfixedView 在一开始介绍原理的时候我们说过了。而 contentView 则表示 ScrollView 中的内容,它用于实现上拉刷新,暂时还用不到,我们等实现相关功能的时候再说;
  • bounds 用于表示每个 View 自身的尺寸,稍后,我们用这个值计算不同 View 之间的距离。和之前保存 Tag 数据不同的是,这次,没有把 CGRect 包装在 Anchor 里。主要是因为稍后我们获取数据的方式要求 PrefData 是一个实现了 Equatable 的类型,但是 Apple 在 SwiftUI 的正式版中暂时去掉了 AnchorEquatable 的支持。大家现在知道有这么回事儿就行了,稍后在实现的时候,我们再具体说;

接下来,就是 PrefKey 了。它的 Value[PrefData],默认值是空数组。还记得之前我们说过 Value 的类型通常都是个数组么,这里再一次印证了我们之前的说法。而 reduce 的实现,就是向数组中添加元素。如果你看过之前 Tag 的实现,这部分内容就应该很好理解,除了要保存的数据稍微复杂一些之外,这些代码和 Tag 的处理方式几乎是一样的。

这时,如果你到 GitHub 去看代码,可能还会看到 ContentPrefDataContentPrefKey。同样,它们是用于实现上拉刷新的,因此,先忽略它们就好了。

ActivityIndicator

准备好数据部分之后,我们来看个工具 ActivityIndicator,它定义在 ActivityIndicator.swift 里:

struct ActivityIndicator: UIViewRepresentable {

  func makeUIView(
    context: UIViewRepresentableContext<ActivityIndicator>)
    -> UIActivityIndicatorView {
    return UIActivityIndicatorView()
  }

  func updateUIView(
    _ uiView: UIActivityIndicatorView,
    context: UIViewRepresentableContext<ActivityIndicator>) {
    uiView.startAnimating()
  }
}

其实它的实现没什么好说的,就是把 UIKit 中的 UIActivityIndicatorView 嫁接到 SwiftUI 的一份标准实现。通过 updateUIView 方法,我们让 UIActivityIndicatorView 一显示出来,就是旋转的状态。

What's next?

至此,前期的准备工作就差不多了,下一节,我们来实现用于 BxRefreshableScrollView 上的一些辅助 UI 组件。

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

¥ 59

按月订阅

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

开始订阅

¥ 512

按年订阅

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

开始订阅

¥ 1280

泊学终身会员

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

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