这一节开始,我们来分享如何基于 SwftUI 实现支持下拉和上拉刷新的滚动视图。这是个在泊学 App 很多界面上都会用到的功能。由于它的实现相对独立,我们为这个 UI 组件单独创建了一个开源项目:BxRefreshableScrollView。
应该说,这个实现方式多多少少有点 hack 的味道,它的实现思路是这样的。由于无法直接通过 ScrollView 获取滚动位置的数据,我们在 SwiftUI 的 ScrollView 上,覆盖了两个 View:一个高度为 0,它随着 ScrollView 的滚动而滚动,我们叫它 MovingView;另一个高度和 ScrollView 自身相同,并且位置固定,我们叫它 FixedView:

这样,当滚动的时候,我们只要计算 MovingView 和 FixedView 顶部之间的距离,当它超过某个值时,就在顶部开始显示某个 View 并触发一个动作。就实现下拉刷新的效果了。
虽然道理听着挺简单,实现它还是稍微有一点点麻烦。而用到的主要技术,就是之前我们提到过的 PreferenceKey 和 PreferenceData。
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中定义的,它分三种情况。movingView和fixedView在一开始介绍原理的时候我们说过了。而contentView则表示ScrollView中的内容,它用于实现上拉刷新,暂时还用不到,我们等实现相关功能的时候再说;bounds用于表示每个View自身的尺寸,稍后,我们用这个值计算不同View之间的距离。和之前保存Tag数据不同的是,这次,没有把CGRect包装在Anchor里。主要是因为稍后我们获取数据的方式要求PrefData是一个实现了Equatable的类型,但是 Apple 在 SwiftUI 的正式版中暂时去掉了Anchor对Equatable的支持。大家现在知道有这么回事儿就行了,稍后在实现的时候,我们再具体说;
接下来,就是 PrefKey 了。它的 Value 是 [PrefData],默认值是空数组。还记得之前我们说过 Value 的类型通常都是个数组么,这里再一次印证了我们之前的说法。而 reduce 的实现,就是向数组中添加元素。如果你看过之前 Tag 的实现,这部分内容就应该很好理解,除了要保存的数据稍微复杂一些之外,这些代码和 Tag 的处理方式几乎是一样的。
这时,如果你到 GitHub 去看代码,可能还会看到 ContentPrefData 和 ContentPrefKey。同样,它们是用于实现上拉刷新的,因此,先忽略它们就好了。
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 组件。