阅读 893

Swift 开发 wanandroid 客户端——RxSwift封装MJRefresh

这是我参与更文挑战的第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)
复制代码

而后,我仔细阅读了大佬的代码,抛弃自己写的这块封装,直接上全部使用大佬的代码,给大佬献上膝盖!!!

MJRefresh-RxSwift

直接把Sources中的代码拖入项目中即可使用。

代码改造

在使用了大佬写好的封装好,我对RxSwift编写的积分排名页面做了改造,下面通过sourcetree截图给出的改造情况,实际的改造量,并不大:

Xnip2021-06-21_10-17-39.jpg

源码我也附上,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的页面改变。

大家加油。

文章分类
iOS
文章标签