Swift 開發 wanandroid 客戶端——頁面下拉刷新和上拉加載功能

這是我參與更文挑戰的第20天,活動詳情查看: 更文挑戰

實現下拉刷新與上拉加載功能

在昨天的代碼中,咱們經過RxSwift對積分排行榜的第一頁進行網絡請求和數據返回,而後使用數據去驅動頁面的加載。編程

固然僅僅1頁的加載,對於一個有分頁功能的頁面是根本是沒有意義,作到作好下拉與上拉功能很是重要。數組

這裏咱們須要分析一下幾點:markdown

  • 集成什麼控件去作下拉刷新與上拉加載?網絡

    • MJRefresh,該控件雖然是OC寫的,可是調用與封裝都比較完善,新老手均可以使用。

  • 下拉刷新邏輯:ide

    • 下拉刷新是要將page的頁數重置爲第1頁,重置footer的狀態。佈局

    • 對第1頁的數據進行網絡請求,將獲取的數據賦值給數據源dataSource,讓其驅動頁面。post

    • 網絡請求完成,注意無論是成功仍是失敗都應該結束下拉刷新的狀態。spa

    • 請求完第一頁須要判斷是否有下頁,保持foot的顯示與狀態:代理

      • 這裏使用的是玩安卓後臺返回的兩個字段來判斷curPage與pageCount,若是相等就說明是最後一頁,沒有更多數據,若是curPage小於pageCount,說明還有下一頁。

  • 上拉加載更多邏輯:
    • 上拉加載是要將page的頁數加1。code

    • 對第page + 1頁的數據進行網絡請求,將獲取的數據與以前的dataSourc進行合併,注意是合併,而不是直接賦值,讓其驅動頁面。

    • 網絡請求完成,注意無論是成功仍是失敗都應該結束上拉加載更多的狀態。

    • 請求完第page + 1頁須要判斷是否有下頁,保持foot的顯示與狀態:

      • 這裏使用的是玩安卓後臺返回的兩個字段來判斷curPage與pageCount,若是相等就說明是最後一頁,沒有更多數據,若是curPage小於pageCount,說明還有下一頁。

好了上面的分析作完了,那麼就按照這個思路修改代碼了,請注意看代碼註釋喔

import UIKit

import RxSwift
import RxCocoa
import NSObject_Rx
import Moya
import MJRefresh


class RxSwiftCoinRankListController: BaseViewController {
    
    /// 懶加載tableView
    private lazy var tableView = UITableView(frame: .zero, style: .plain)
    
    /// 初始化page爲1
    private var page: Int = 1
    
    /// 既是可監聽序列也是觀察者的數據源
    private var dataSource: BehaviorRelay<[CoinRank]> = BehaviorRelay(value: [])
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    private func setupTableView() {
        
        /// 設置tableFooterView
        tableView.tableFooterView = UIView()
        
        /// 設置代理
        tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
        
        /// 設置頭部刷新控件
        tableView.mj_header = MJRefreshNormalHeader()
        
        tableView.mj_header?.beginRefreshing { [weak self] in
            self?.refreshAction()
        }
        
        /// 設置尾部刷新控件
        tableView.mj_footer = MJRefreshBackNormalFooter()
        
        tableView.mj_footer?.beginRefreshing { [weak self] in
            self?.loadMoreAction()
        }
        
        /// 簡單佈局
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalTo(view)
        }
        
        /// 數據源驅動
        dataSource
            .asDriver(onErrorJustReturn: [])
            .drive(tableView.rx.items) { (tableView, row, coinRank) in
            if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") {
                cell.textLabel?.text = coinRank.username
                cell.detailTextLabel?.text = coinRank.coinCount?.toString
                return cell
            }else {
                let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
                cell.textLabel?.text = coinRank.username
                cell.detailTextLabel?.text = coinRank.coinCount?.toString
                return cell
            }
        }
        .disposed(by: rx.disposeBag)
    }

}

extension RxSwiftCoinRankListController {
    /// 下拉刷新行爲
    private func refreshAction() {
        resetCurrentPageAndMjFooter()
        getCoinRank(page: page)
    }
    
    /// 上拉加載更多行爲
    private func loadMoreAction() {
        page = page + 1
        getCoinRank(page: page)
    }
    
    /// 下拉的參數與狀態重置行爲
    private func resetCurrentPageAndMjFooter() {
        page = 1
        self.tableView.mj_footer?.isHidden = false
        self.tableView.mj_footer?.resetNoMoreData()
    }
    
    /// 網絡請求
    private func getCoinRank(page: Int) {
        myProvider.rx.request(MyService.coinRank(page))
            /// 轉Model
            .map(BaseModel<Page<CoinRank>>.self)
            /// 因爲須要使用Page,因此return到$0.data這一層,而不是$0.data.datas
            .map{ $0.data }
            /// 解包
            .compactMap { $0 }
            /// 轉換操做
            .asObservable()
            .asSingle()
            /// 訂閱
            .subscribe { event in
                
                /// 訂閱事件
                /// 經過page的值判斷是下拉仍是上拉(能夠用枚舉),無論成功仍是失敗都結束刷新狀態
                page == 1 ? self.tableView.mj_header?.endRefreshing() : self.tableView.mj_footer?.endRefreshing()
                
                switch event {
                case .success(let pageModel):
                    /// 解包數據
                    if let datas = pageModel.datas {
                        /// 經過page的值判斷是下拉仍是上拉,作數據處理,這裏爲了方便寫註釋,沒有使用三目運算符
                        if page == 1 {
                            /// 下拉作賦值運算
                            self.dataSource.accept(datas)
                        }else {
                            /// 上拉作合併運算
                            self.dataSource.accept(self.dataSource.value + datas)
                        }
                    }
                    
                    /// 解包curPage與pageCount
                    if let curPage = pageModel.curPage, let pageCount = pageModel.pageCount  {
                        /// 若是發現它們相等,說明是最後一個,改變foot而狀態
                        if curPage == pageCount {
                            self.tableView.mj_footer?.endRefreshingWithNoMoreData()
                        }
                    }
                case .error(_):
                    /// error佔時不作處理
                    break
                }
            }.disposed(by: rx.disposeBag)
    }
}

extension RxSwiftCoinRankListController: UITableViewDelegate {}

複製代碼

以上代碼已經寫完了完善的註釋,這裏單獨說一下:

private var dataSource: BehaviorRelay<[CoinRank]> = BehaviorRelay(value: [])中的dataSource

不一樣於通常的dataSource是一個數組,這裏咱們使用了RxSwift中的BehaviorRelay,它既是一個序列也能夠是一個觀察者,而且能夠對數據進行賦值運算。序列能夠轉爲特化序列Driver,並驅動tableView,能夠作賦值運算,因而能夠將網絡請求的數據進行賦值和合並操做,在我上面的代碼中很是關鍵。

那麼下一步是?

其實上面的代碼運行起來沒有什麼問題,只是並不RxSwifty,沒有那種rx.xxxx回調的感受。

我我的的理解是,有的是時候不能光顧着面子上的事,先保證功能沒有問題了,再來考慮拓展與深度。掌握好基礎的知識與技能是基石。

同時,在寫上面的代碼的時候,我也在考慮如何用一個值去綁定tableView,經過狀態來改變header與footer的UI狀態。

這個其實和聲明式UI編寫的原則一致了,UI = f(state)

明日繼續

我繼續圍繞着MJRefresh與下拉刷新和上拉加載,考慮使用RxSwift對其進行一層,來進行更好Rx編程。

爲啥我會抓着一個簡單的列表不放:玩安卓App不少頁面都是列表,寫好一個,其餘的均可以按照這個思路編寫與複用。

你們加油。

相關文章
相關標籤/搜索