iOS 實現簡單的列表預加載

在大部分 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,也能夠自定義不一樣的策略實現。

相關文章
相關標籤/搜索