参照 RxSwift + MJRefresh 打造自动处理刷新控件状态 这位大佬的文章,有点抄袭的味道。
枚举
首先定义一个有关刷新状态的枚举类型:
/// 可按照自己的需求添加,由于我没有用到 mj_footer.beginRefreshing(),
/// 所以没有定义相关的枚举。
enum RefreshStatus {case nonecase beingHeaderRefreshcase endHeaderRefreshcase endFooterRefresh// 这个枚举由于在我项目中经常用到,所以我定一个关联值的枚举。// 项目中需要:// - 数据为空的时候隐藏 `mj_footer`,否则显示;// - 然后判断没有更多数据就调用 `endRefreshingWithNoMoreData()`// 否则 `endRefreshing()`case footerStatus(isHidden: Bool, isNoMoreData: Bool)
}
无需过多纠结,后面会演示枚举如何使用。
BehaviorSubject
接下来要介绍一个跟 RxSwift 有关的一个类型 BehaviorSubject
,我们会在文章用到它。
BehaviorSubject
向所有订阅者发布事件,并向新的订阅者提供最近(或最初)的值。
怎么理解?来看看代码:
func addObserver(_ id: String) -> Disposable {return subscribe { print("Subscription:", id, "Event:", $0) }
}let disposeBag = DisposeBag()
let subject = BehaviorSubject(value: "?")subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("?")
subject.onNext("?")subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("?️")
subject.onNext("?️")/**
Subscription: 1 Event: next(?)
Subscription: 1 Event: next(?)
Subscription: 1 Event: next(?)
Subscription: 2 Event: next(?)
Subscription: 1 Event: next(?️)
Subscription: 2 Event: next(?️)
Subscription: 1 Event: next(?️)
Subscription: 2 Event: next(?️)
Subscription: 1 Event: next(?)
Subscription: 2 Event: next(?)
Subscription: 1 Event: next(?)
Subscription: 2 Event: next(?)
*/
总结一下:
BehaviorSubject
从上至下接收它发出的最新(原始值也属于发出的事件元素)值。并向新的订阅者提供最新值,所以这里我们 订阅2号 会接收到前面发出的最新元素,稍后才是 订阅2号 自己发出的元素事件。
这里请允许我搬布官方的图例来说明一下:
如你所见,紫灯的时候订阅,会接收到紫灯以及之后的所有元素。绿灯(可以理解为我们的 订阅2号,也就是传说中的新订阅者)的时候订阅,接收到绿灯以及之后的所有元素。
图片来源:reactivex.io/documentati…,对 RxSwift 感兴趣的同学也可以看下我最近发布的 RxSwift 系列文章。
协议
接下来我们要用到协议,用来封装和刷新状态有关的东西。对 Swift 协议还不是太明白的可以继续看上面那位大佬写的文章:
iOS - Swift 面向协议编程(一)
iOS - Swift 面向协议编程(二)
开始
好了,下面我们就用上面学到的所有知识来写一个自动管理刷新状态案例。
假设有这样一个需求:
// ViewController.swift// 两个闭包的参数默认为 nil,根据参数自动创建 mj_header 或 mj_footer,
// 不传参数则不创建。自动管理刷新状态。
viewModel.refreshStatusBind(to: tableView, {// 处理头部刷新。
}) {// 处理尾部刷新。
}.disposed(by: bag)
如何做到这一点?看到方法是从 viewModel
里面调出来的,那我们就去 viewModel
里面看一看究竟。
class ViewModel: Refreshable {lazy var list &#61; Variable<[MnlDakaCommentModel]>([])let refreshStatus &#61; BehaviorSubject(value: RefreshStatus.none)let reload &#61; PublishSubject<Bool>()let bag &#61; DisposeBag()init() {reload.subscribe(onNext: { [weak self] (isDown) inguard let &#96;self&#96; &#61; self else {return}// 发送请求MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in let list &#61; result.value?["list"].arrayObjectlet models &#61; decode([MnlDakaCommentModel].self, from: list) ?? []}}}
}
好了&#xff0c;为了简洁删除了部分代码&#xff0c;但该有的还是有&#xff0c;而且 ViewModel
里面我确实没有创建 refreshStatusBind(to:)
方法。
这就奇怪了&#xff0c;究竟方法从何而来&#xff1f;答案在于协议。注意我们开始签了一个 Refreshable
的协议&#xff0c;refreshStatusBind(to:)
是在里面定义的。那我们就去看看&#xff0c;这个方法究竟是什么&#xff1f;为什么传几个参数进去就能自动创建刷新控件并管理其状态了呢&#xff1f;
Refreshable
首先&#xff0c;我们定义了一个 Refreshable
协议&#xff1a;
protocol Refreshable {var refreshStatus: BehaviorSubject<RefreshStatus> { get }
}
这里你就知道了吧&#xff1f;任何实现 Refreshable
必须实现 refreshStatus
属性&#xff0c;如果你足够眼尖应该看到&#xff0c;上面的 ViewModel
同样定义一个类型一样 refreshStatus
属性&#xff0c;为的就是实现协议中规定的属性。
好了&#xff0c;有个这个属性之后&#xff0c;我们就可以愉快的管理刷新状态了&#xff0c;比如想让它结束刷新&#xff0c;我们可以拿到 refreshStatus
属性&#xff0c;比如在 ViewModel
里&#xff0c;我们可以这样&#xff1a;
// 发送请求
MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in let list &#61; result.value?["list"].arrayObjectlet models &#61; decode([MnlDakaCommentModel].self, from: list) ?? []// 请求完成需要结束刷新&#xff1a;// refreshStatus.onNext(.endFooterRefresh)// 或者判断没有更多数据时&#xff1a;// refreshStatus.onNext(isHidden: false, isNoMoreData: true)
}
这时你肯定问了&#xff0c;凭什么我这样发送消息就可以管理刷新状态了&#xff1f;你逗我呢&#xff1f;之前讲 BehaviorSubject
的时候不是有讲到订阅 (subscribe
) 吗&#xff1f;既然这里发送消息了&#xff0c;肯定会在接收到发出的元素的时候做了什么处理吧&#xff1f;
问得好&#xff01;问得非常好&#xff01;问得太————好了。好吧&#xff0c;我老实交代&#xff0c;就来说下接收到元素时我都做了什么&#xff1f;
extension Refreshable {func refreshStatusBind(to scrollView: UIScrollView, _ header: (() -> Void)? &#61; nil, _ footer: (() -> Void)? &#61; nil) -> Disposable {if header !&#61; nil {scrollView.mj_header &#61; MJRefreshNormalHeader {// 处理头部方法时结束尾部刷新。scrollView.mj_footer?.endRefreshing()header?()}}if footer !&#61; nil {scrollView.mj_footer &#61; MJRefreshAutoNormalFooter {// 处理尾部方法时结束头部刷新。scrollView.mj_header?.endRefreshing()footer?()}}return refreshStatus.subscribe(onNext: { (status) inswitch status {case .none:// 未发生任何状态事件时隐藏尾部。scrollView.mj_footer?.isHidden &#61; truecase .beginHeaderRefresh:scrollView.mj_header?.beginRefreshing()case .endHeaderRefresh:scrollView.mj_header?.endRefreshing()case .endFooterRefresh:scrollView.mj_footer?.endRefreshing()case .endAllRefresh:// 结束全部拉刷新scrollView.mj_header?.endRefreshing()scrollView.mj_footer?.endRefreshing()case .footerStatus(let isHidden, let isNone):// 根据关联值确定 footer 的状态。scrollView.mj_footer?.isHidden &#61; isHidden// 处理尾部状态时&#xff0c;如果之前正在刷新头部&#xff0c;则结束刷新&#xff0c;// 至此&#xff0c;我们无需写判断结束头部刷新的代码&#xff0c;在这里自动处理。scrollView.mj_header?.endRefreshing()if isNone {scrollView.mj_footer?.endRefreshingWithNoMoreData()}else {scrollView.mj_footer?.endRefreshing()}}})}
}
给 Refreshable
加一个扩展&#xff0c;我们先来看方法的第一部分&#xff1a;创建头部和尾部刷新控件。这段代码很容易看懂&#xff0c;结合前面放在 ViewController.swift
里的代码&#xff1a;
// 两个闭包的参数默认为 nil&#xff0c;根据参数自动创建 mj_header 或 mj_footer&#xff0c;
// 不传参数则不创建。自动管理刷新状态。
viewModel.refreshStatusBind(to: tableView, {// 处理头部刷新。
}) {// 处理尾部刷新。
}.disposed(by: bag)
无非就是传闭包参数的时候创建对应的刷新控件&#xff0c;并把传进去的闭包作为控件的刷新事件。因为我已经把 UIScrollView
作为参数传进来了&#xff0c;所以可以直接拿到它创建刷新控件。
在到第二部分&#xff0c;这就是一个真正监听状态改变的部分了&#xff0c;是根据发出的消息做出改变的反应。这样&#xff0c;我们在 ViewModel
里发送消息&#xff0c;这里就能接收到并作出对应的改变。
Demo
贴下完整代码。
ViewController.swift
import UIKit
import RxSwiftclass ViewController: UITableViewController {lazy var viewModel &#61; ViewModel()let bag &#61; DisposeBag()override func viewDidLoad() {super.viewDidLoad()/// 创建刷新控件。viewModel.refreshStatusBind(to: tableView, { [weak self] in// 处理头部刷新。self?.viewModel.reload.onNext(false)}) { [weak self] in// 处理尾部刷新。self?.viewModel.reload.onNext(true)}.disposed(by: bag)// 给 viewModel 中的 reload 发送消息&#xff0c;让其请求数据。// 参数 Bool 表示是否上拉。viewModel.reload.onNext(false)}
}
ViewModel.swift
import UIKit
import RxSwiftclass ViewModel: Refreshable {lazy var list &#61; Variable<[Model]>([])let refreshStatus &#61; BehaviorSubject(value: RefreshStatus.none)let reload &#61; PublishSubject<Bool>()let bag &#61; DisposeBag()init() {reload.subscribe(onNext: { [weak self] (isReload) inguard let &#96;self&#96; &#61; self else {return}MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) inlet list &#61; result.value?["list"].arrayObjectlet models &#61; decode([MnlDakaCommentModel].self, from: list) ?? []self.list.value &#61; isReload ? models : self.list.value &#43; modelslet count &#61; result.value?["count"].int ?? 0// 发送刷新状态给订阅者&#xff0c;让其作出改变。// 如果列表个数和总数相等&#xff0c;则判断它为没有更多数据。self.refreshStatus.onNext(.footerStatus(isHidden: self.list.value.isEmpty,isNoMoreData: self.list.value.count &#61;&#61; count))}}).disposed(by: bag)}
}
关于
DisposeBag
&#xff0c;其实就是一个资源回收包&#xff0c; 使用Rx
代码会占用一些资源&#xff0c;我们把这些资源都添加到bag
里面&#xff0c;这样在其对应的引用被deinit
后&#xff0c;资源会被回收。详情可以查阅官方文档&#xff1a; Disposing section。