这是我参与更文挑战的第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很多页面都是列表,写好一个,其他的都可以按照这个思路编写与复用。
大家加油。