分頁列表是開發(fā)過程中最常見的的需求,雖然簡單,但是還是有一些點值得總結(jié)。之前也沒有過多思考過這個需求,每次都是想到那寫到哪,結(jié)果就是經(jīng)常會犯之前翻過的錯誤,所以決定總結(jié)出一個模版出來。
值得注意的點
- 兩個動作:下拉刷新,上拉加載更多。
下拉刷新需要注意,刷新前不能清空數(shù)據(jù),而是要等刷新的數(shù)據(jù)返回之后去替換原來的就數(shù)據(jù)。否則當(dāng)刷新失敗,刷新前的數(shù)據(jù)又被清空,就會導(dǎo)致頁面沒有數(shù)據(jù)。
上拉加載更多要注意當(dāng)沒有更多數(shù)據(jù)時,我們應(yīng)該把刷新狀態(tài)設(shè)置為沒有更多數(shù)據(jù),可以避免無效刷新。 - 刷新過程
控制器需要感知到刷新狀態(tài)(閑置/刷新中/刷新結(jié)束)的變化,以展示合適的UI。
模版

模版.png
- 控制器/視圖
業(yè)務(wù)的載體,給用戶提供服務(wù)。
lazy var manager = ListManager()
lazy var refreshHeader: MJRefreshNormalHeader = {
let header = MJRefreshNormalHeader { [weak self] in
self?.manager.reload()
}
return header
}()
lazy var refreshFooter: MJRefreshAutoStateFooter = {
let footer = MJRefreshAutoStateFooter { [weak self] in
self?.manager.loadNextPage()
}
return footer
}()
private func handleState(_ state: ListManager.State) {
switch state {
case .idle:
refreshHeader.endRefreshing()
if manager.isExhausted {
refreshFooter.endRefreshingWithNoMoreData()
} else {
refreshFooter.endRefreshing()
}
case .loading:
break
case .loaded(let error):
tableView.reloadData()
print(error?.localizedDescription ?? "")
}
}
-
Manager
封裝核心數(shù)據(jù)邏輯管理的類,是業(yè)務(wù)的骨架,給控制器提供服務(wù)。
這里有下拉刷新和下拉加載兩個動作可以引起頁面變化,但是如果我們從數(shù)據(jù)層面來看的話,其實是這兩個動作引起了列表數(shù)據(jù)變化,數(shù)據(jù)變化引起了頁面變化。所以其實控制器只關(guān)心數(shù)據(jù)和獲取數(shù)據(jù)的進度狀態(tài)。所以我引入了一個Manager去處理這中間的轉(zhuǎn)換。Manager提供reload、loadNextPage接口,然后輸出列表數(shù)據(jù)和刷新狀態(tài)。
Controller & Manager
class ListManager {
/// 刷新狀態(tài)
enum State {
/// 閑置
case idle
/// 加載中
case loading
/// 加載結(jié)束(error為nil時表示刷新成功)
case loaded(Error?)
}
/// 是否還有數(shù)據(jù)
var isExhausted: Bool = false
/// 列表數(shù)據(jù)
private(set) var list = [ListModel]()
/// 狀態(tài)
private(set) var state: State = .idle {
didSet {
statehandler?(state)
}
}
/// 狀態(tài)閉包(用于通知控制器獲取數(shù)據(jù)的狀態(tài))
var statehandler: ((_ state: State) -> Void)?
/// 下拉刷新調(diào)用的方法
func reload() {
loadData(offset: 0) { [weak self] items in
self?.list = items
}
}
/// 上拉加載下一頁調(diào)用的方法
func loadNextPage() {
loadData(offset: list.count) { [weak self] items in
self?.list.append(contentsOf: items)
}
}
/// 獲取數(shù)據(jù),處理狀態(tài),處理isExhausted
private func loadData(offset: Int, onSuccess: ((_ list: [ListModel]) -> Void)?) {
state = .loading
ListSourceService.loadListData(offset: offset, limit: ListSourceService.limit) { [weak self] list in
guard let self = self else {
return
}
onSuccess?(list)
self.isExhausted = list.count < ListSourceService.limit
self.state = .loaded(nil)
self.state = .idle
} onError: { [weak self] error in
self?.state = .loaded(error)
self?.state = .idle
}
}
}
- Service
數(shù)據(jù)來源,給Manger提供服務(wù)。
struct ListSourceService {
/// 每頁數(shù)量
static var limit = 10
static let totolCount = 55
/// 獲取數(shù)據(jù)方法(這里用延時模擬獲取數(shù)據(jù))
static func loadListData(offset: Int, limit: Int, onSuccess: @escaping ((_ list: [ListModel]) -> Void), onError: ((_ error: Error) -> Void)) {
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
var items = [ListModel]()
let count = Self.totolCount - offset
guard count > 0 else {
onSuccess(items)
return
}
let remain = min(Self.limit, count)
for i in 0..<remain {
let model = ListModel(name: "第\(i+offset+1)條數(shù)據(jù)")
items.append(model)
}
onSuccess(items)
}
}
}
總結(jié)
這樣做可以達(dá)到數(shù)據(jù)邏輯和UI分離、數(shù)據(jù)驅(qū)動UI的效果。數(shù)據(jù)邏輯和UI分離使得對其任一部分的修改都不會影響另一部分;數(shù)據(jù)驅(qū)動UI使得代碼可讀性、維護性會更高。
