在大部分 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) { (_, _) in guard let delegate = self.delegate else { return } self.strategy.totalRowsCount = delegate.totalRowsCount } contentOffsetObserver = observe(\.scrollView.contentOffset){ (_, _) in let offsetY = self.scrollView.contentOffset.y + self.scrollView.frame.height let totalHeight = self.scrollView.contentSize.height guard offsetY < totalHeight else { return } if self.strategy.shouldFetch(totalHeight, offsetY) { self.delegate?.startFetch() } } } public func stop() { contentSizeObserver?.invalidate() contentOffsetObserver?.invalidate() } public init(strategy:ListPrefetcherStrategy, scrollView:UIScrollView) { self.strategy = strategy self.scrollView = scrollView }}复制代码
这样外界使用起来只需要提供策略和 scrollView,实现 delegate 的方法,然后在需要的时候 start 和 stop,就可以自动完成预加载的工作了。
最后
完整的 ,也可以自定义不同的策略实现。