当前位置:   article > 正文

MJRefresh和RxSwift

rxswift mjrefresh
插播一条小广告.orz

  我的个人项目: iOS仿写有妖气漫画(组件化架构+响应式编程) 已经正式启动啦.jpg。

关于RxSwift

  对RxSwift不熟悉的同学可以查看这两篇文档:

这两篇文档翻译的都非常好,小伙伴们多多练习多多体会每个操作符,Rx系列其实也并不是那么难学(打不开的同学可以问我要电子书)。

MJRefresh的窘境

  MJRefresh相信从事iOS开发的小伙伴们都很熟悉了,是由李明杰老师开源的下拉刷新上拉加载的第三方库。它使用的是cocoa中非常常见的target-action模式。先来看一眼传统的使用方式:

  1. // 初始化一个header
  2. tableView.mj_header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(loadData))
  3. // 设置刷新的回调
  4. @objc func loadData() {
  5. // 发起网络请求,balabala...
  6. }
  7. 复制代码

这种使用方式在经典的MVC架构下并没有太多问题(MVC结构下网络层代码无处安放,只有ViewController稍微合适,这块的内容网上大书特书,我就不瞎BB了)。

  而在MVVM结构下,网络请求相关逻辑被移入了ViewModel。稍微扯几句MVVM,MVVM下View是知道ViewModel的,因为要执行数据绑定更新UI,而ViewModel是不知道View的,否则耦合就比较严重,ViewModel不能独立测试,MVVM的优势就荡然无存了。   接着上面的话题,传统的使用方式上:

  1. @objc func loadData() {
  2. // 发起网络请求,balabala...
  3. API.loadData(success: { (responseObj) in
  4. // 1. 处理返回的数据
  5. // balabala...
  6. // 2. 关闭mj_header/mj_footer的刷新状态
  7. self.tableView.mj_header.endRefreshing()
  8. }, failure: { (error) in
  9. // 1. 处理错误
  10. // balabala...
  11. // 2. 同样要关闭刷新状态
  12. self.tableView.mj_header.endRefreshing()
  13. })
  14. 复制代码

Command+R运行良好,可以泡杯茶休息一下了~~ 慢着慢着,如果是MVVM,那么在ViewModel中就会是:

  1. static func loadData() -> Observable<Data> {
  2. return Observable<Data>.create({ (observer) in
  3. let task = URLSession.shared.dataTask(with: URLRequest(url: URL(string: "http://balabala...")!), completionHandler: { (data, _, error) in
  4. // 处理出错
  5. guard error != nil else {
  6. observer.onError(error!)
  7. return
  8. }
  9. // 处理出错
  10. guard let data = data else {
  11. observer.onError(NSError(domain: "com.archer.errorDomain", code: 250, userInfo: nil))
  12. return
  13. }
  14. // 请求成功 返回数据
  15. observer.onNext(data)
  16. observer.onCompleted()
  17. })
  18. task.resume()
  19. return Disposables.create { task.cancel() }
  20. })
  21. }
  22. 复制代码

那么问题来了,ViewModel中是不能持有View的,那么在这里就不能直接停止mj_header/mj_footer的刷新状态。又要返回View Controller在订阅的地方处理吗?像这样?

  1. API.loadData()
  2. .subscribe(onNext: { (data) in
  3. // 处理数据
  4. // balabala...
  5. }, onError: { (error) in
  6. // 1. 处理出错
  7. // balabala...
  8. // 2. 停止刷新
  9. self.tableView.mj_header.endRefreshing()
  10. }, onCompleted: {
  11. // 停止刷新
  12. self.tableView.mj_header.endRefreshing()
  13. }).disposed(by: disposeBag)
  14. 复制代码

对这个简单的请求来说可以是可以,可是这一点也不Rx。如果是一个返回给RxTableViewSectionedReloadDataSource的Observable呢?

  1. API.loadData() // 假设返回Observable<SectionModel>
  2. .bind(to: tableView.rx.items(dataSource: dataSouce))
  3. .disposed(by: disposeBag)
  4. 复制代码

emmm...没有地方处理刷新控件的状态了。聪明的你又想到了再写一遍API.loadData().subscribe去处理。zzZ~~简单来说这样会触发两次网络请求,因为Rx本身并不保持状态,你需要这样:

  1. // 使用share操作符来共享状态
  2. let mObservable = API.loadData().share(replay: 1)
  3. mObservable
  4. .bind(to: tableView.rx.items(dataSource: dataSouce))
  5. .disposed(by: disposeBag)
  6. mObservable
  7. .subscribe(onNext: { (data) in
  8. // ...
  9. }, onError: { (error) in
  10. // ...
  11. }, onCompleted: {
  12. // ...
  13. }).disposed(by: disposeBag)
  14. 复制代码

好吧,这样的代码已经和优雅不沾边了。

RxSwift结合MJRefresh

  废话了半天,终于引出我们的主角了。简单总结一下我们的需求:在用户下拉tableView到一定距离,MJRefresh通知我们它已经进入刷新状态,此时可以去发起请求了,在请求成功结束或失败的时候,我们通知MJRefresh结束其刷新状态,这样就完成了一次具体下拉刷新操作。   查看一下MJRefresh的源码,MJRefreshHeader和MJRefreshFooter均继承自MJRefreshComponent,在MJRefreshComponent中定义了一个枚举:

  1. /** 刷新控件的状态 */
  2. typedef NS_ENUM(NSInteger, MJRefreshState) {
  3. /** 普通闲置状态 */
  4. MJRefreshStateIdle = 1,
  5. /** 松开就可以进行刷新的状态 */
  6. MJRefreshStatePulling,
  7. /** 正在刷新中的状态 */
  8. MJRefreshStateRefreshing,
  9. /** 即将刷新的状态 */
  10. MJRefreshStateWillRefresh,
  11. /** 所有数据加载完毕,没有更多的数据了 */
  12. MJRefreshStateNoMoreData
  13. };
  14. /** 刷新状态 一般交给子类内部实现 */
  15. @property (assign, nonatomic) MJRefreshState state;
  16. 复制代码

就是这个state控制了整个刷新控件的状态,实例方法beginRefreshing(), endRefreshing(), endRefreshingWithNoMoreData()均是改变state属性。

  1. #pragma mark 进入刷新状态
  2. - (void)beginRefreshing
  3. {
  4. [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
  5. self.alpha = 1.0;
  6. }];
  7. self.pullingPercent = 1.0;
  8. // 只要正在刷新,就完全显示
  9. if (self.window) {
  10. self.state = MJRefreshStateRefreshing;
  11. } else {
  12. // 预防正在刷新中时,调用本方法使得header inset回置失败
  13. if (self.state != MJRefreshStateRefreshing) {
  14. self.state = MJRefreshStateWillRefresh;
  15. // 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下)
  16. [self setNeedsDisplay];
  17. }
  18. }
  19. }
  20. #pragma mark 结束刷新状态
  21. - (void)endRefreshing
  22. {
  23. dispatch_async(dispatch_get_main_queue(), ^{
  24. self.state = MJRefreshStateIdle;
  25. });
  26. }
  27. - (void)endRefreshingWithNoMoreData
  28. {
  29. dispatch_async(dispatch_get_main_queue(), ^{
  30. self.state = MJRefreshStateNoMoreData;
  31. });
  32. }
  33. 复制代码

  MJRefreshComponent的子类都是根据这个state来改变自身状态。明白了原理,接下来的目标就相对明确了。我们需要这个state通知我们何时发起请求,又需要通知这个state结束刷新,因此它需要同时是Observable和Observer。RxCocoa中为我们提供的ControlProperty刚好满足这个需求。   翻阅一下RxCocoa,UITextFiled的rx.text属性就实现为ControlProperty,让我们看一下它是怎么实现的:

  1. public func controlProperty<T>(
  2. editingEvents: UIControlEvents,
  3. getter: @escaping (Base) -> T,
  4. setter: @escaping (Base, T) -> ()
  5. ) -> ControlProperty<T> {
  6. // 创建Observable
  7. let source: Observable<T> = Observable.create { [weak weakControl = base] observer in
  8. // base被销毁就结束流
  9. guard let control = weakControl else {
  10. observer.on(.completed)
  11. return Disposables.create()
  12. }
  13. // 发出初始值
  14. observer.on(.next(getter(control)))
  15. let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
  16. if let control = weakControl {
  17. // editingEvent触发时发出下一个值
  18. observer.on(.next(getter(control)))
  19. }
  20. }
  21. return Disposables.create(with: controlTarget.dispose)
  22. }
  23. // 流的生命周期和base一致
  24. .takeUntil(deallocated)
  25. let bindingObserver = Binder(base, binding: setter)
  26. return ControlProperty<T>(values: source, valueSink: bindingObserver)
  27. }
  28. 复制代码

最后的实现为这么一个泛型函数,传递的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就很简单了,直接上代码。

  1. // RxTarget类并不是公开API 我们自己实现一下就好了
  2. class Target: NSObject, Disposable {
  3. private var retainSelf: Target?
  4. override init() {
  5. super.init()
  6. self.retainSelf = self
  7. }
  8. func dispose() {
  9. self.retainSelf = nil
  10. }
  11. }
  12. // 自定义target,用来接收MJRefresh的刷新事件
  13. private final
  14. class MJRefreshTarget<Component: MJRefreshComponent>: Target {
  15. weak var component: Component?
  16. let refreshingBlock: MJRefreshComponentRefreshingBlock
  17. init(_ component: Component , refreshingBlock: @escaping MJRefreshComponentRefreshingBlock) {
  18. self.refreshingBlock = refreshingBlock
  19. self.component = component
  20. super.init()
  21. component.setRefreshingTarget(self, refreshingAction: #selector(onRefeshing))
  22. }
  23. @objc func onRefeshing() {
  24. refreshingBlock()
  25. }
  26. override func dispose() {
  27. super.dispose()
  28. self.component?.refreshingBlock = nil
  29. }
  30. }
  31. // 扩展Rx 给MJRefreshComponent 添加refresh的rx扩展
  32. extension Reactive where Base: MJRefreshComponent {
  33. var refresh: ControlProperty<MJRefreshState> {
  34. let source: Observable<MJRefreshState> = Observable.create { [weak component = self.base] observer in
  35. MainScheduler.ensureExecutingOnScheduler()
  36. guard let component = component else {
  37. observer.on(.completed)
  38. return Disposables.create()
  39. }
  40. // 发出初始值MJRefreshStateIdle
  41. observer.on(.next(component.state))
  42. let observer = MJRefreshTarget(component) {
  43. // 在用户下拉时 发出MJRefreshComponent 的状态
  44. observer.on(.next(component.state))
  45. }
  46. return observer
  47. }.takeUntil(deallocated)
  48. // 在setter里设置MJRefreshComponent 的状态
  49. // 当一个Observable<MJRefreshState>发出,假如这个state是MJRefreshStateIdle,那么MJRefreshComponent 就会结束刷新
  50. let bindingObserver = Binder<MJRefreshState>(self.base) { (component, state) in
  51. component.state = state
  52. }
  53. return ControlProperty(values: source, valueSink: bindingObserver)
  54. }
  55. }
  56. 复制代码

几乎就是照葫芦画瓢了~~ 再来预习一下使用:

  1. func bind(reactor: ViewControllerReactor) {
  2. // 如果发出一个refreshing事件,就发起请求
  3. // 这里就是用户下拉tableview了
  4. tableView.mj_header
  5. .rx.refresh
  6. .filter { $0 == .refreshing }
  7. .map { _ in Reactor.Action.refresh }
  8. .bind(to: reactor.action)
  9. .disposed(by: disposeBag)
  10. // 点击按钮转换成发出refreshing事件 refreshing已绑定到Reactor.Action.Refresh
  11. // 触发mj_header刷新 然后请求数据
  12. navigationItem.rightBarButtonItem?.rx.tap
  13. .map { MJRefreshState.refreshing }
  14. .bind(to: tableView.mj_header.rx.refresh)
  15. .disposed(by: disposeBag)
  16. // 绑定tableview数据源
  17. reactor.state
  18. .map { $0.sectionModels }
  19. .bind(to: tableView.rx.items(dataSource: dataSouce))
  20. .disposed(by: disposeBag)
  21. // 根据返回的状态控制mj_header的状态
  22. reactor.state
  23. .map { $0.refreshingState }
  24. .bind(to: tableView.mj_header.rx.refresh)
  25. .disposed(by: disposeBag)
  26. }
  27. 复制代码

  这里使用了ReactorKit而不是MVVM,关于ReactorKit大家可以去Github上看看不难使用。最后附上代码MJRefresh+Rx

转载于:https://juejin.im/post/5c2dcaaa51882501c1662d46

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/229293
推荐阅读
  

闽ICP备14008679号