插播一条小广告.orz
我的个人项目: iOS仿写有妖气漫画(组件化架构+响应式编程) 已经正式启动啦.jpg。
关于RxSwift
对RxSwift不熟悉的同学可以查看这两篇文档:
这两篇文档翻译的都非常好,小伙伴们多多练习多多体会每个操作符,Rx系列其实也并不是那么难学(打不开的同学可以问我要电子书)。
MJRefresh的窘境
MJRefresh相信从事iOS开发的小伙伴们都很熟悉了,是由李明杰老师开源的下拉刷新上拉加载的第三方库。它使用的是cocoa中非常常见的target-action模式。先来看一眼传统的使用方式:
- // 初始化一个header
- tableView.mj_header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(loadData))
-
- // 设置刷新的回调
- @objc func loadData() {
- // 发起网络请求,balabala...
- }
- 复制代码
这种使用方式在经典的MVC架构下并没有太多问题(MVC结构下网络层代码无处安放,只有ViewController稍微合适,这块的内容网上大书特书,我就不瞎BB了)。
而在MVVM结构下,网络请求相关逻辑被移入了ViewModel。稍微扯几句MVVM,MVVM下View是知道ViewModel的,因为要执行数据绑定更新UI,而ViewModel是不知道View的,否则耦合就比较严重,ViewModel不能独立测试,MVVM的优势就荡然无存了。 接着上面的话题,传统的使用方式上:
- @objc func loadData() {
- // 发起网络请求,balabala...
- API.loadData(success: { (responseObj) in
- // 1. 处理返回的数据
- // balabala...
- // 2. 关闭mj_header/mj_footer的刷新状态
- self.tableView.mj_header.endRefreshing()
- }, failure: { (error) in
- // 1. 处理错误
- // balabala...
- // 2. 同样要关闭刷新状态
- self.tableView.mj_header.endRefreshing()
- })
- 复制代码
Command+R运行良好,可以泡杯茶休息一下了~~ 慢着慢着,如果是MVVM,那么在ViewModel中就会是:
- static func loadData() -> Observable<Data> {
- return Observable<Data>.create({ (observer) in
- let task = URLSession.shared.dataTask(with: URLRequest(url: URL(string: "http://balabala...")!), completionHandler: { (data, _, error) in
- // 处理出错
- guard error != nil else {
- observer.onError(error!)
- return
- }
- // 处理出错
- guard let data = data else {
- observer.onError(NSError(domain: "com.archer.errorDomain", code: 250, userInfo: nil))
- return
- }
- // 请求成功 返回数据
- observer.onNext(data)
- observer.onCompleted()
- })
- task.resume()
- return Disposables.create { task.cancel() }
- })
- }
- 复制代码
那么问题来了,ViewModel中是不能持有View的,那么在这里就不能直接停止mj_header/mj_footer的刷新状态。又要返回View Controller在订阅的地方处理吗?像这样?
- API.loadData()
- .subscribe(onNext: { (data) in
- // 处理数据
- // balabala...
- }, onError: { (error) in
- // 1. 处理出错
- // balabala...
- // 2. 停止刷新
- self.tableView.mj_header.endRefreshing()
- }, onCompleted: {
- // 停止刷新
- self.tableView.mj_header.endRefreshing()
- }).disposed(by: disposeBag)
- 复制代码
对这个简单的请求来说可以是可以,可是这一点也不Rx。如果是一个返回给RxTableViewSectionedReloadDataSource的Observable呢?
- API.loadData() // 假设返回Observable<SectionModel>
- .bind(to: tableView.rx.items(dataSource: dataSouce))
- .disposed(by: disposeBag)
- 复制代码
emmm...没有地方处理刷新控件的状态了。聪明的你又想到了再写一遍API.loadData().subscribe去处理。zzZ~~简单来说这样会触发两次网络请求,因为Rx本身并不保持状态,你需要这样:
- // 使用share操作符来共享状态
- let mObservable = API.loadData().share(replay: 1)
- mObservable
- .bind(to: tableView.rx.items(dataSource: dataSouce))
- .disposed(by: disposeBag)
- mObservable
- .subscribe(onNext: { (data) in
- // ...
- }, onError: { (error) in
- // ...
- }, onCompleted: {
- // ...
- }).disposed(by: disposeBag)
- 复制代码
好吧,这样的代码已经和优雅不沾边了。
RxSwift结合MJRefresh
废话了半天,终于引出我们的主角了。简单总结一下我们的需求:在用户下拉tableView到一定距离,MJRefresh通知我们它已经进入刷新状态,此时可以去发起请求了,在请求成功结束或失败的时候,我们通知MJRefresh结束其刷新状态,这样就完成了一次具体下拉刷新操作。 查看一下MJRefresh的源码,MJRefreshHeader和MJRefreshFooter均继承自MJRefreshComponent,在MJRefreshComponent中定义了一个枚举:
- /** 刷新控件的状态 */
- typedef NS_ENUM(NSInteger, MJRefreshState) {
- /** 普通闲置状态 */
- MJRefreshStateIdle = 1,
- /** 松开就可以进行刷新的状态 */
- MJRefreshStatePulling,
- /** 正在刷新中的状态 */
- MJRefreshStateRefreshing,
- /** 即将刷新的状态 */
- MJRefreshStateWillRefresh,
- /** 所有数据加载完毕,没有更多的数据了 */
- MJRefreshStateNoMoreData
- };
-
- /** 刷新状态 一般交给子类内部实现 */
- @property (assign, nonatomic) MJRefreshState state;
- 复制代码
就是这个state控制了整个刷新控件的状态,实例方法beginRefreshing(), endRefreshing(), endRefreshingWithNoMoreData()均是改变state属性。
- #pragma mark 进入刷新状态
- - (void)beginRefreshing
- {
- [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
- self.alpha = 1.0;
- }];
- self.pullingPercent = 1.0;
- // 只要正在刷新,就完全显示
- if (self.window) {
- self.state = MJRefreshStateRefreshing;
- } else {
- // 预防正在刷新中时,调用本方法使得header inset回置失败
- if (self.state != MJRefreshStateRefreshing) {
- self.state = MJRefreshStateWillRefresh;
- // 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下)
- [self setNeedsDisplay];
- }
- }
- }
-
- #pragma mark 结束刷新状态
- - (void)endRefreshing
- {
- dispatch_async(dispatch_get_main_queue(), ^{
- self.state = MJRefreshStateIdle;
- });
- }
-
- - (void)endRefreshingWithNoMoreData
- {
- dispatch_async(dispatch_get_main_queue(), ^{
- self.state = MJRefreshStateNoMoreData;
- });
- }
- 复制代码
MJRefreshComponent的子类都是根据这个state来改变自身状态。明白了原理,接下来的目标就相对明确了。我们需要这个state通知我们何时发起请求,又需要通知这个state结束刷新,因此它需要同时是Observable和Observer。RxCocoa中为我们提供的ControlProperty刚好满足这个需求。 翻阅一下RxCocoa,UITextFiled的rx.text属性就实现为ControlProperty,让我们看一下它是怎么实现的:
- public func controlProperty<T>(
- editingEvents: UIControlEvents,
- getter: @escaping (Base) -> T,
- setter: @escaping (Base, T) -> ()
- ) -> ControlProperty<T> {
- // 创建Observable
- let source: Observable<T> = Observable.create { [weak weakControl = base] observer in
- // base被销毁就结束流
- guard let control = weakControl else {
- observer.on(.completed)
- return Disposables.create()
- }
- // 发出初始值
- observer.on(.next(getter(control)))
-
- let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
- if let control = weakControl {
- // editingEvent触发时发出下一个值
- observer.on(.next(getter(control)))
- }
- }
-
- return Disposables.create(with: controlTarget.dispose)
- }
- // 流的生命周期和base一致
- .takeUntil(deallocated)
-
- let bindingObserver = Binder(base, binding: setter)
-
- return ControlProperty<T>(values: source, valueSink: bindingObserver)
- }
- 复制代码
最后的实现为这么一个泛型函数,传递的editingEvent是[.allEditingEvents, .valueChanged]。函数内部首先创建了一个Observable,泛型参数T对于UITextFiled的rx.text属性来说是String?。创建Observable的过程中保持了一个对调用者自身的弱引用来避免循环引用,接着首先检查调用者是否被销毁,如果被销毁直接结束流,如果没有就创建一个ControlTarget来接收传递的editingEvent。看一下ControlTarget的源码,它做的事情很简单,说白了它就是一个接收事件的target,回调的selector把事件转发给了初始化参数Callback。每当editingEvent触发时,它都发出一个值,对UITextFiled来说就是取出它当前的text发出去(通过gette来包装)。Binder就更简单了,每当有新值时,通过setter设置新值也就是设置UITextFiled的text。 整个流程理清了以后,实现RxMJRefresh就很简单了,直接上代码。
- // RxTarget类并不是公开API 我们自己实现一下就好了
- class Target: NSObject, Disposable {
- private var retainSelf: Target?
- override init() {
- super.init()
- self.retainSelf = self
- }
- func dispose() {
- self.retainSelf = nil
- }
- }
-
- // 自定义target,用来接收MJRefresh的刷新事件
- private final
- class MJRefreshTarget<Component: MJRefreshComponent>: Target {
- weak var component: Component?
- let refreshingBlock: MJRefreshComponentRefreshingBlock
-
- init(_ component: Component , refreshingBlock: @escaping MJRefreshComponentRefreshingBlock) {
- self.refreshingBlock = refreshingBlock
- self.component = component
- super.init()
- component.setRefreshingTarget(self, refreshingAction: #selector(onRefeshing))
- }
-
- @objc func onRefeshing() {
- refreshingBlock()
- }
-
- override func dispose() {
- super.dispose()
- self.component?.refreshingBlock = nil
- }
- }
-
- // 扩展Rx 给MJRefreshComponent 添加refresh的rx扩展
- extension Reactive where Base: MJRefreshComponent {
- var refresh: ControlProperty<MJRefreshState> {
- let source: Observable<MJRefreshState> = Observable.create { [weak component = self.base] observer in
- MainScheduler.ensureExecutingOnScheduler()
- guard let component = component else {
- observer.on(.completed)
- return Disposables.create()
- }
-
- // 发出初始值MJRefreshStateIdle
- observer.on(.next(component.state))
-
- let observer = MJRefreshTarget(component) {
- // 在用户下拉时 发出MJRefreshComponent 的状态
- observer.on(.next(component.state))
- }
- return observer
- }.takeUntil(deallocated)
-
- // 在setter里设置MJRefreshComponent 的状态
- // 当一个Observable<MJRefreshState>发出,假如这个state是MJRefreshStateIdle,那么MJRefreshComponent 就会结束刷新
- let bindingObserver = Binder<MJRefreshState>(self.base) { (component, state) in
- component.state = state
- }
- return ControlProperty(values: source, valueSink: bindingObserver)
- }
- }
- 复制代码
几乎就是照葫芦画瓢了~~ 再来预习一下使用:
- func bind(reactor: ViewControllerReactor) {
- // 如果发出一个refreshing事件,就发起请求
- // 这里就是用户下拉tableview了
- tableView.mj_header
- .rx.refresh
- .filter { $0 == .refreshing }
- .map { _ in Reactor.Action.refresh }
- .bind(to: reactor.action)
- .disposed(by: disposeBag)
-
- // 点击按钮转换成发出refreshing事件 refreshing已绑定到Reactor.Action.Refresh
- // 触发mj_header刷新 然后请求数据
- navigationItem.rightBarButtonItem?.rx.tap
- .map { MJRefreshState.refreshing }
- .bind(to: tableView.mj_header.rx.refresh)
- .disposed(by: disposeBag)
-
- // 绑定tableview数据源
- reactor.state
- .map { $0.sectionModels }
- .bind(to: tableView.rx.items(dataSource: dataSouce))
- .disposed(by: disposeBag)
-
- // 根据返回的状态控制mj_header的状态
- reactor.state
- .map { $0.refreshingState }
- .bind(to: tableView.mj_header.rx.refresh)
- .disposed(by: disposeBag)
- }
- 复制代码
这里使用了ReactorKit而不是MVVM,关于ReactorKit大家可以去Github上看看不难使用。最后附上代码MJRefresh+Rx。