枚举
首先定义一个有关刷新状态的枚举类型:
/// 可按照自己的需求添加,由于我没有用到 mj_footer.beginRefreshing(),
/// 所以没有定义相关的枚举。
enum RefreshStatus {
case none
case beingHeaderRefresh
case endHeaderRefresh
case 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号 自己发出的元素事件。
开始
好了,下面我们就用上面学到的所有知识来写一个自动管理刷新状态案例。
假设有这样一个需求:
// ViewController.swift
// 两个闭包的参数默认为 nil,根据参数自动创建 mj_header 或 mj_footer,
// 不传参数则不创建。自动管理刷新状态。
viewModel.refreshStatusBind(to: tableView, {
// 处理头部刷新。
}) {
// 处理尾部刷新。
}.disposed(by: bag)
如何做到这一点?看到方法是从 viewModel 里面调出来的,那我们就去 viewModel 里面看一看究竟。
class ViewModel: Refreshable {
lazy var list = Variable<[MnlDakaCommentModel]>([])
let refreshStatus = BehaviorSubject(value: RefreshStatus.none)
let reload = PublishSubject<Bool>()
let bag = DisposeBag()
init() {
reload.subscribe(onNext: { [weak self] (isDown) in
guard let `self` = self else {
return
}
// 发送请求
MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in
let list = result.value?["list"].arrayObject
let models = decode([MnlDakaCommentModel].self, from: list) ?? []
}
}
}
}
这里你就知道了吧?任何实现 Refreshable 必须实现 refreshStatus 属性,如果你足够眼尖应该看到,上面的 ViewModel 同样定义一个类型一样 refreshStatus 属性,为的就是实现协议中规定的属性。
好了,有个这个属性之后,我们就可以愉快的管理刷新状态了,比如想让它结束刷新,我们可以拿到 refreshStatus 属性,比如在 ViewModel 里,我们可以这样:
// 发送请求
MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in
let list = result.value?["list"].arrayObject
let models = decode([MnlDakaCommentModel].self, from: list) ?? []
// 请求完成需要结束刷新:
// refreshStatus.onNext(.endFooterRefresh)
// 或者判断没有更多数据时:
// refreshStatus.onNext(isHidden: false, isNoMoreData: true)
}
这时你肯定问了,凭什么我这样发送消息就可以管理刷新状态了?你逗我呢?之前讲 BehaviorSubject 的时候不是有讲到订阅 (subscribe) 吗?既然这里发送消息了,肯定会在接收到发出的元素的时候做了什么处理吧?
问得好!问得非常好!问得太————好了。好吧,我老实交代,就来说下接收到元素时我都做了什么?
extension Refreshable {
func refreshStatusBind(to scrollView: UIScrollView, _ header: (() -> Void)? = nil, _ footer: (() -> Void)? = nil) -> Disposable {
if header != nil {
scrollView.mj_header = MJRefreshNormalHeader {
// 处理头部方法时结束尾部刷新。
scrollView.mj_footer?.endRefreshing()
header?()
}
}
if footer != nil {
scrollView.mj_footer = MJRefreshAutoNormalFooter {
// 处理尾部方法时结束头部刷新。
scrollView.mj_header?.endRefreshing()
footer?()
}
}
return refreshStatus.subscribe(onNext: { (status) in
switch status {
case .none:
// 未发生任何状态事件时隐藏尾部。
scrollView.mj_footer?.isHidden = true
case .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 = isHidden
// 处理尾部状态时,如果之前正在刷新头部,则结束刷新,
// 至此,我们无需写判断结束头部刷新的代码,在这里自动处理。
scrollView.mj_header?.endRefreshing()
if isNone {
scrollView.mj_footer?.endRefreshingWithNoMoreData()
}else {
scrollView.mj_footer?.endRefreshing()
}
}
})
}
}
给 Refreshable 加一个扩展,我们先来看方法的第一部分:创建头部和尾部刷新控件。这段代码很容易看懂,结合前面放在 ViewController.swift 里的代码:
// 两个闭包的参数默认为 nil,根据参数自动创建 mj_header 或 mj_footer,
// 不传参数则不创建。自动管理刷新状态。
viewModel.refreshStatusBind(to: tableView, {
// 处理头部刷新。
}) {
// 处理尾部刷新。
}.disposed(by: bag)
无非就是传闭包参数的时候创建对应的刷新控件,并把传进去的闭包作为控件的刷新事件。因为我已经把 UIScrollView 作为参数传进来了,所以可以直接拿到它创建刷新控件。
在到第二部分,这就是一个真正监听状态改变的部分了,是根据发出的消息做出改变的反应。这样,我们在 ViewModel 里发送消息,这里就能接收到并作出对应的改变。
完整代码
ViewController.swift
import UIKit
import RxSwift
class ViewController: UITableViewController {
lazy var viewModel = ViewModel()
let bag = 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 发送消息,让其请求数据。
// 参数 Bool 表示是否上拉。
viewModel.reload.onNext(false)
}
}
ViewModel.swift
import UIKit
import RxSwift
class ViewModel: Refreshable {
lazy var list = Variable<[Model]>([])
let refreshStatus = BehaviorSubject(value: RefreshStatus.none)
let reload = PublishSubject<Bool>()
let bag = DisposeBag()
init() {
reload.subscribe(onNext: { [weak self] (isReload) in
guard let `self` = self else {
return
}
MnlAssetLoader.load(.dakaComment(params: self.params)) { (result) in
let list = result.value?["list"].arrayObject
let models = decode([MnlDakaCommentModel].self, from: list) ?? []
self.list.value = isReload ? models : self.list.value + models
let count = result.value?["count"].int ?? 0
// 发送刷新状态给订阅者,让其作出改变。
// 如果列表个数和总数相等,则判断它为没有更多数据。
self.refreshStatus.onNext(.footerStatus(isHidden: self.list.value.isEmpty,
isNoMoreData: self.list.value.count == count))
}
}).disposed(by: bag)
}
}