在大部分 App 中,在有 feeds 流之类列表的地方,由于后端数据一般采用分页加载,为了用户体验需要做预加载。最简单的加载方式,就是当列表显示的内容达到一定的数量时候,自动请求下一个分页。
加载策略
而这其实就是根据总行数,列表总高度,列表当前偏移值这三个数字决定是否要加载的关系式 fx。这里判断加载的策略,是需要自定义的,所以可以定义这样一个 Protocol。
protocol ListPrefetcherStrategy {
var totalRowsCount:Int { get set }
func shouldFetch(_ totalHeight:CGFloat, _ offsetY:CGFloat) -> Bool
}
下面给出几种简单的加载策略。
阈值策略
设定一个阈值,比如 70%,显示内容达到阈值时进行加载。这种比较时候每一页的数量一致的情况。
同时要注意的是,这里的阈值应该是每个分页的阈值,总的阈值会随着列表长度增长。比如设置阈值为 70%,每页加载 10 个,第一页在加载到 7 个时进行预加载,第二页在第 17 个时进行预加载,此时阈值为 85%,而如果还是 70%,则会在第 14 个时进行预加载。所以这里的阈值需要动态增长。
假设我们已知目前列表的数据量和目前页数,根据每一页的阈值就可以动态计算总阈值:
// 数据总数除以当前页数,算出每一页的数量
let perPageCount = Double(totalRowsCount) / Double(currentPageIndex + 1)
// 每页数量乘以页数加上每一页的阈值的和,就是总共需要的数量
let needRowsCount = perPageCount * (Double(currentPageIndex) + threshold)
// 算出动态的阈值
let actalThreshold = needRowsCount / Double(totalRowsCount)
这里需要记录当前的页数,笔者这里用了一个比较 trick 的做法,当行数增长时,则认为页数 +1,行数减少时,则认为页数归 0,适用于下拉刷新整个列表清空的情况。可以用属性观察 willSet 来改变页数。
struct ThresholdStrategy: ListPrefetcherStrategy{
func shouldFetch(_ totalHeight: CGFloat, _ offsetY: CGFloat) -> Bool {
let viewRatio = Double(offsetY / totalHeight)
let perPageCount = Double(totalRowsCount) / Double(currentPageIndex + 1)
let needRowsCount = perPageCount * (Double(currentPageIndex) + threshold)
let actalThreshold = needRowsCount / Double(totalRowsCount)
if viewRatio >= actalThreshold {
return true
} else {
return false
}
}
var totalRowsCount: Int{
willSet{
if newValue > totalRowsCount {
currentPageIndex += 1
} else if newValue < totalRowsCount {
currentPageIndex = 0
}
}
}
let threshold: Double
var currentPageIndex = 0
public init(threshold:Double = 0.7) {
self.threshold = threshold
totalRowsCount = 0
}
}
剩余策略
也可以设定当列表剩余未展示行数即将少于某个值时,进行加载。这种适合每次分页数量不一定一致的情况。
struct RemainStrategy: ListPrefetcherStrategy{
func shouldFetch(_ totalHeight: CGFloat, _ offsetY: CGFloat) -> Bool {
let rowHeight = totalHeight / CGFloat(totalRowsCount)
let needOffsetY = rowHeight * CGFloat(totalRowsCount - remainRowsCount)
if offsetY > needOffsetY {
return true
} else {
return false
}
}
var totalRowsCount: Int
let remainRowsCount: Int
init(remainRowsCount:Int = 1) {
self.remainRowsCount = remainRowsCount
totalRowsCount = 0
}
}
除法策略
还可以自己定义除数和余数,当达到余数时,进行加载。当然还要考虑一下实际余数比指定余数小的情况,这里笔者简单的往前面偏移一个除数的量进行判断。
struct OffsetStrategy: ListPrefetcherStrategy {
func shouldFetch(_ totalHeight: CGFloat, _ offsetY: CGFloat) -> Bool {
let rowHeight = totalHeight / CGFloat(totalRowsCount)
let actalOffset = totalRowsCount % gap
let needOffsetY = actalOffset > offset ? totalHeight - CGFloat(actalOffset - offset) * rowHeight : totalHeight - CGFloat(2 * gap + offset) * rowHeight
if offsetY > needOffsetY {
return true
} else {
return false
}
}
var totalRowsCount: Int
let gap: Int
let offset: Int
init(gap:Int, offset:Int) {
self.gap = gap
self.offset = offset
totalRowsCount = 0
}
}
预加载组件
组件需要的信息有,scrollView,总行数,以及加载时候的通知外界。
定义一个 delegate 让外界遵循。
protocol ListPrefetcherDelegate:AnyObject {
var totalRowsCount:Int { get }
func startFetch()
}
然后用 KVO 监听 scrollView 的 contentSize,当发生变化是,就认为总行数发生改变,就可以将总行数设置给策略。监听 scrollView 的 contentOffset,变化时就是列表滚动,就可以用策略进行判断。
class ListPrefetcher:NSObject{
@objc let scrollView:UIScrollView
var contentSizeObserver:NSKeyValueObservation?
var contentOffsetObserver:NSKeyValueObservation?
weak var delegate: ListPrefetcherDelegate?
var strategy: ListPrefetcherStrategy
public func start() {
contentSizeObserver = observe(\.scrollView.contentSize) { object, _ in
guard let delegate = object.delegate else { return }
object.strategy.totalRowsCount = delegate.totalRowsCount
}
contentOffsetObserver = observe(\.scrollView.contentOffset){ object, _ in
let offsetY = object.scrollView.contentOffset.y + object.scrollView.frame.height
let totalHeight = object.scrollView.contentSize.height
guard offsetY < totalHeight else { return }
if object.strategy.shouldFetch(totalHeight, offsetY) {
object.delegate?.startFetch()
}
}
}
public func stop() {
contentSizeObserver?.invalidate()
contentOffsetObserver?.invalidate()
}
public init(strategy:ListPrefetcherStrategy, scrollView:UIScrollView) {
self.strategy = strategy
self.scrollView = scrollView
super.init()
}
}
这样外界使用起来只需要提供策略和 scrollView,实现 delegate 的方法,然后在需要的时候 start 和 stop,就可以自动完成预加载的工作了。
最后
完整的 Demo,也可以自定义不同的策略实现。