Swift:玩安卓App——页面下拉刷新和上拉加载功能

这是我参与更文挑战的第20天,活动详情查看: 更文挑战

实现下拉刷新与上拉加载功能

在昨天的代码中,我们通过RxSwift对积分排行榜的第一页进行网络请求和数据返回,然后使用数据去驱动页面的加载。

当然仅仅1页的加载,对于一个有分页功能的页面是根本是没有意义,做到做好下拉与上拉功能非常重要。

这里我们需要分析一下几点:

  • 集成什么控件去做下拉刷新与上拉加载?

    • MJRefresh,该控件虽然是OC写的,但是调用与封装都比较完善,新老手都可以使用。

  • 下拉刷新逻辑:

    • 下拉刷新是要将page的页数重置为第1页,重置footer的状态。

    • 对第1页的数据进行网络请求,将获取的数据赋值给数据源dataSource,让其驱动页面。

    • 网络请求完成,注意不管是成功还是失败都应该结束下拉刷新的状态。

    • 请求完第一页需要判断是否有下页,保持foot的显示与状态:

      • 这里使用的是玩安卓后台返回的两个字段来判断curPage与pageCount,如果相等就说明是最后一页,没有更多数据,如果curPage小于pageCount,说明还有下一页。

  • 上拉加载更多逻辑:
    • 上拉加载是要将page的页数加1。

    • 对第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对其进行一层,来进行更好的编程。

为啥我会抓着一个简单的列表不放:玩安卓App很多页面都是列表,写好一个,其他的都可以按照这个思路编写与复用。

大家加油。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享