这一节开始,我们来分享如何基于 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 组件。