在大部分 App 中,在有 feeds 流之類列表的地方,因爲後端數據通常採用分頁加載,爲了用戶體驗須要作預加載。最簡單的加載方式,就是當列表顯示的內容達到必定的數量時候,自動請求下一個分頁。git
而這其實就是根據總行數,列表總高度,列表當前偏移值這三個數字決定是否要加載的關係式 fx。這裏判斷加載的策略,是須要自定義的,因此能夠定義這樣一個 Protocol。github
protocol ListPrefetcherStrategy {
var totalRowsCount:Int { get set }
func shouldFetch(_ totalHeight:CGFloat, _ offsetY:CGFloat) -> Bool
}
複製代碼
下面給出幾種簡單的加載策略。swift
設定一個閾值,好比 70%,顯示內容達到閾值時進行加載。這種比較時候每一頁的數量一致的狀況。後端
同時要注意的是,這裏的閾值應該是每一個分頁的閾值,總的閾值會隨着列表長度增加。好比設置閾值爲 70%,每頁加載 10 個,第一頁在加載到 7 個時進行預加載,第二頁在第 17 個時進行預加載,此時閾值爲 85%,而若是仍是 70%,則會在第 14 個時進行預加載。因此這裏的閾值須要動態增加。fetch
假設咱們已知目前列表的數據量和目前頁數,根據每一頁的閾值就能夠動態計算總閾值:spa
// 數據總數除以當前頁數,算出每一頁的數量
let perPageCount = Double(totalRowsCount) / Double(currentPageIndex + 1)
// 每頁數量乘以頁數加上每一頁的閾值的和,就是總共須要的數量
let needRowsCount = perPageCount * (Double(currentPageIndex) + threshold)
// 算出動態的閾值
let actalThreshold = needRowsCount / Double(totalRowsCount)
複製代碼
這裏須要記錄當前的頁數,筆者這裏用了一個比較 trick 的作法,當行數增加時,則認爲頁數 +1,行數減小時,則認爲頁數歸 0,適用於下拉刷新整個列表清空的狀況。能夠用屬性觀察 willSet 來改變頁數。code
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
}
}
複製代碼
也能夠設定當列表剩餘未展現行數即將少於某個值時,進行加載。這種適合每次分頁數量不必定一致的狀況。server
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
}
}
複製代碼
還能夠本身定義除數和餘數,當達到餘數時,進行加載。固然還要考慮一下實際餘數比指定餘數小的狀況,這裏筆者簡單的往前面偏移一個除數的量進行判斷。rem
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,總行數,以及加載時候的通知外界。get
定義一個 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,也能夠自定義不一樣的策略實現。