在大部分 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,就可以自動完成預載入的工作了。
最後
完整的 Demo,也可以自定義不同的策略實現。