这是我参与更文挑战的第21天,活动详情查看: 更文挑战
先上一个RxSwift封装UIButton的例子
凡事都是先易后难,所以,在尝试通过RxSwift封装MJRefresh前,我们先看看RxCocoa框架对于系统组件的封装,这样有利于我们进行仿写(其实是CV啦)。
使用RxSwift编写按钮的点击事件
let button = UIButton(type: .custom)
button.rx.tap.subscribe { _ in
print("按钮的点击事件")
}.disposed(by: rx.disposeBag)
这里我们可以看见button.rx.tap这种方式的处理,而且它可以被订阅,说明是其返回是一个序列。
至于rx.tap这种书写风格,在Swift中其实很多地方都很常见,就我之前阅读过Kingfisher的源码,便知道这是通过协议对类进行的命名空间的扩展,具体分析不在这里扩展。
于是乎我们继续看看这个tap到底是怎么封装的:
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
return controlEvent(.touchUpInside)
}
}
关于ControlEvent
ControlEvent它是一种特化的序列。
ControlEvent 专门用于描述 UI 控件所产生的事件,它具有以下特征:
- 不会产生 error 事件
- 一定在 MainScheduler 订阅(主线程订阅)
- 一定在 MainScheduler 监听(主线程监听)
- 共享附加作用
既然是序列,那么订阅就是可行的。
接下来让我们想想怎么给MJRefresh做个封装吧。
RxSwift封装MJRefresh
下拉与上拉行为封装
我们先看看tableView.mj_header中的mj_header是什么类型,直接点源码即可知道:
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
通过runtime在UIScrollView中添加了属性,类型为MJRefreshHeader。
那么我们开始写扩展的时候,先可以这样命名:
extension Reactive where Base: MJRefreshHeader {
var refresh: ControlEvent<Void> {
/// 这里的实现是怎么写,是个难题
}
}
但是里面的实现怎么写,对于我一个RxSwift新手是个难题,不过没关系,自己不会写,可以去搜索嘛,看了别的代码,也许就有思路了。
extension Reactive where Base: MJRefreshHeader {
var refresh: ControlEvent<Void> {
let source: Observable<Void> = Observable.create {
[weak control = self.base] observer in
if let control = control {
control.refreshingBlock = {
observer.on(.next(()))
}
}
return Disposables.create()
}
return ControlEvent(events: source)
}
}
上面代码中,source是一个序列,于是要是生成一个序列,我们要使用特定的工厂方法——Observable.create{},拿到所谓的base,其实就是MJRefreshHeader自己。
接着MJRefreshHeader的回调中观察者去操作行为,然后最后通过ControlEvent的初始化方法封装成一个ControlEvent序列。
于是我们可以这样用了:
tableView.mj_header?.rx.refresh
.subscribe { [weak self] _ in
/// 刷新事件
self?.refreshAction()
}.disposed(by: rx.disposeBag)
你可能甚至觉得,这么写,还不如原生Api来的简洁:
tableView.mj_header?.beginRefreshing { [weak self] in
self?.refreshAction()
}
其实我自己也是这么想的,不过这个例子中,我们更着重看的是封装的思路。
既然mj_header封装好了,那么其实mj_footer就是依葫芦画瓢啦。
虽然依葫芦画瓢简单,但是理清了mj_header与mj_footer的继承关系,会让代码更加干练:
@interface MJRefreshHeader : MJRefreshComponent
@interface MJRefreshFooter : MJRefreshComponent
既然MJRefreshHeader与MJRefreshFooter都是继承于MJRefreshComponent,那么封装一次就就可以使用了:
extension Reactive where Base: MJRefreshComponent {
var refresh: ControlEvent<Void> {
let source: Observable<Void> = Observable.create {
[weak control = self.base] observer in
if let control = control {
control.refreshingBlock = {
observer.on(.next(()))
}
}
return Disposables.create()
}
return ControlEvent(events: source)
}
}
这样的话mj_header与mj_footer的行为封装就统一好了:
/// 下拉刷新
tableView.mj_header?.rx.refresh
.subscribe { [weak self] _ in
self?.refreshAction()
}.disposed(by: rx.disposeBag)
/// 上拉加载
tableView.mj_footer?.rx.refresh
.subscribe { [weak self] _ in
self?.loadMoreAction()
}.disposed(by: rx.disposeBag)
状态关系绑定
如果说上面的行为封装,只是RxSwift运用上的锦上添花,那么对于tableView刷新状态的封装,并与tableView的绑定,就是这次封装的核心问题了。
大概的思路就是包裹了状态的序列与tableView中扩展定义的Binder属性做绑定,以达到目的。
我自己思来想去了好久,主要还是对于状态与UI的转变关系难以理顺,最后在网上看到了下面这段代码:
/// 供ViewModel使用
enum MJRefreshAction {
/// 开始刷新
case begainRefresh
/// 停止刷新
case stopRefresh
/// 开始加载更多
case begainLoadmore
/// 停止加载更多
case stopLoadmore
/// 显示无更多数据
case showNomoreData
/// 重置无更多数据
case resetNomoreData
}
//MARK:- Refresh
extension Reactive where Base: UIScrollView {
/// 执行的操作类型
var refreshAction: Binder<MJRefreshAction> {
return Binder(base) { (target, action) in
switch action{
case .begainRefresh:
if let header = target.mj_header {
header.beginRefreshing()
}
case .stopRefresh:
if let header = target.mj_header {
header.endRefreshing()
}
case .begainLoadmore:
if let footer = target.mj_footer {
footer.beginRefreshing()
}
case .stopLoadmore:
if let footer = target.mj_footer {
footer.endRefreshing()
}
case .showNomoreData:
if let footer = target.mj_footer {
footer.endRefreshingWithNoMoreData()
}
case .resetNomoreData:
if let footer = target.mj_footer {
footer.resetNoMoreData()
}
}
}
}
}
这段代码的使用如下:
/// 在控制器中定一个refreshSubject 既是可监听序列也是观察者的状态枚举
private let refreshSubject: BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: .begainRefresh)
/// 做绑定操作
/// 下拉与上拉状态绑定到tableView
refreshSubject
.bind(to: tableView.rx.refreshAction)
.disposed(by: rx.disposeBag)
而后,我仔细阅读了大佬的代码,抛弃自己写的这块封装,直接上全部使用大佬的代码,给大佬献上膝盖!!!
直接把Sources中的代码拖入项目中即可使用。
代码改造
在使用了大佬写好的封装好,我对RxSwift编写的积分排名页面做了改造,下面通过sourcetree截图给出的改造情况,实际的改造量,并不大:
源码我也附上,MJRefresh-RxSwift:
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
/// 既是可监听序列也是观察者的数据源,里面封装的其实是BehaviorSubject
private let dataSource: BehaviorRelay<[CoinRank]> = BehaviorRelay(value: [])
/// 既是可监听序列也是观察者的状态枚举
private let refreshSubject: BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: .begainRefresh)
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?.rx.refresh
.subscribe { [weak self] _ in
self?.refreshAction()
}.disposed(by: rx.disposeBag)
/// 设置尾部刷新控件
tableView.mj_footer = MJRefreshBackNormalFooter()
tableView.mj_footer?.rx.refresh
.subscribe { [weak self] _ in
self?.loadMoreAction()
}.disposed(by: rx.disposeBag)
/// 简单布局
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)
/// 下拉与上拉状态绑定到tableView
refreshSubject
.bind(to: tableView.rx.refreshAction)
.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
tableView.mj_footer?.isHidden = false
refreshSubject.onNext(.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.refreshSubject.onNext(.stopRefresh) : self.refreshSubject.onNext(.stopLoadmore)
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.refreshSubject.onNext(.showNomoreData)
}
}
case .error(_):
/// error占时不做处理
break
}
}.disposed(by: rx.disposeBag)
}
}
extension RxSwiftCoinRankListController: UITableViewDelegate {}
总结
代码写到这里的时候,我开始思考一些问题。
都是做数据流的绑定,为什么Swift就变得如此的辛苦呢?
在Vue中,我们可以很轻易的通过状态与组件进行绑定。
在Flutter中,虽然已经听人说通过setState去给数据进行赋值进而刷新页面,影响性能,但是setState至少简单用可用呀?
但是到了写iOS中通过RxSwift进行数据绑定,我们要干嘛?需要自己写扩展!!!
虽然RxCocoa对于绝大部分系统组件都做了扩展,但是对于第三方库的支持有限,虽然我也看到了Rx对于非常主流的第三方库做了扩展,比如Moya、Kingfisher等,但是对于不同的App,开发者使用的不同的轮子,RxSwift难以做到支持,需要开发者自己写扩展。
你知道这意味着什么吗?
开发者需要深入理解RxSwift,同时需要深入理解这个第三方(比如今天我们封装的MJRefresh),才能写出逻辑关系正确的扩展,否则可能根本就踏不出这一步。
而这意味着开发者需要花了更多的时间与精力去实现一个功能,而直接使用给好的Api可能更为简单。tableView.mj_header?.rx.refresh.subscribe {}与tableView.mj_header?.beginRefreshing {}就是一个很好的例子。
我本人并不是吹鼓当个Api工程师就好,想要深入学习的前提的是要完成工作,完成了工作才能有空余的时间去学习,如果一开始自己就被这么一道墙挡在门外了,如何进行深入学习呢?
我甚至在想,到底是什么阻挡了数据的绑定模式在Swift中的应用——缺少开箱即用的便利。
即便是现在SwiftUI和Combine出世了,可是好用的状态管理与数据驱动还是没有成型。
那么为何我还在学习RxSwift呢?
思路决定了编码,学习RxSwift可以让我扩展编程的使用,就算在边其他的语言,Rx也的思路可以继续使用,Combine也是官方的RxSwift,明白了RxSwift也会对我有所帮助,就是这样。
明日继续
好了,既然使用RxSwift已经封装好了MJRefresh,是不是这个页面就完成了呢?
听说过MVVM没有,有没有发现我们的代码都写在Controller这一层?
下一步就是抽离数据逻辑到ViewModel层,让Controller只是用将用户行为反馈给ViewModel层,然后ViewModel内部做处理,然后ViewModel的数据去驱动Controller的页面改变。
大家加油。