iOS 實現簡單的列表預載入
在大部分 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 < totalHeightelse { 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,就可以自動完成預載入的工作了。