RxSwift学习RxSwift实战RXSwift 实战

RxSwift+MVVM在快递面单识别Demo中的应用

2017-08-28  本文已影响303人  Nirvana_icy

RxSwift+MVVM在快递面单识别Demo中的应用

GUI编程的特点

1⃣️ 展现数据

2⃣️ 响应用户操作

view = render(state) + handle(event),view 本身只做两件事,给 state 包一层漂亮的外衣,同时对用户的操作做出响应。

快递面单识别Demo中的应用

概述

MVVM with RAC in 美团:

MVVM 最佳使用姿势是搭配RAC/RxSwift等响应式编程框架.

尝试采用 MVVM + RxSwift 来满足GUI编程的特点,模式与美团MVVM + RAC相同.

当用户操作界面时 viewController 捕获到这些事件,然后调用viewModel中的特定方法,这些方法最终导致viewModel中数据的变化,再次反馈到界面上.

MVVM_RAC Error_Handling

经过初步实践 抽离出以下使用姿势:

1⃣️ viewController 四件套

  1. handleDataChange()

  2. handleUIEvent()

  3. configUI()

  4. makeConstraints()

//  DemoWayBillInputVC.swift

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
    self.configUI()            // 配置UI
    self.handleDataChange()    // 响应数据变化
    self.handleUIEvent()       // 响应UI操作
}

2⃣️ 使用Variable包装需要在viewModel和view中绑定的变量

//  DemoWayBillInputVM.swift

struct DemoWayBillInputVM {
    var wayBillStrVariable: Variable<String> = Variable("")
    var phoneNumStrVariable: Variable<String> = Variable("")
}

这样 viewModel中的变量就可以通过 .asObservable() 在VC中进行与UI元素的绑定.

// 响应 viewModel.wayBillStrVariable 变化
        self.viewModel.wayBillStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .filter { !$0.isEmpty }
            .bind(to: self.wayBillTextField.rx.text)
            .disposed(by: disposeBag)

viewModel中的变量 也可通过.value 完成变量值的更新

self.viewModel.wayBillStrVariable.value = text

展现数据 & 响应用户操作

1⃣️ 展现数据

    func handleDataChange() {
        // 响应 viewModel.wayBillStrVariable 变化
        self.viewModel.wayBillStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .filter { !$0.isEmpty }
            .bind(to: self.wayBillTextField.rx.text)
            .disposed(by: disposeBag)

        // tipsStrVariable.value 发生改变后 => tipsLabel.text 随之改变
        self.viewModel.DemoPdaOcrScanViewModel.tipsStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .bind(to: self.scanView.tipsLabel.rx.text)
            .disposed(by: disposeBag)
    }
2⃣️  响应用户操作
    func handleUIEvent() {
        // 记录 wayBillTextField 内容输入
        self.wayBillTextField.rx.text.orEmpty.changed
        .subscribe(onNext: { [unowned self] text in
            self.viewModel.wayBillStrVariable.value = text
          })
        .disposed(by: disposeBag)

        // 闪光灯按钮
        self.scanView.lightControlBtn.rx.tap
            .subscribe(onNext: { [unowned self] in
                self.viewModel.isTorchOn = !self.viewModel.isTorchOn
                self.scanView.lightControlBtn.setTitle(self.viewModel.isTorchOn ? "关闭闪光灯" : "打开闪光灯", for: .normal)
                self.setTorch(torch: self.viewModel.isTorchOn)
            })
            .disposed(by: disposeBag)
      }

🌄 Show Demo & Code

One more thing - 处理页面连跳 with RxSwift

Trello

页面A 跳 B 跳 C Pop to A

避免 block 套 block or Pop时遍历VC

with RxSwift

//  DemoWayBillInputVC.swift

// 发起手机号/条码 扫描录入流程
        self.wayBillScanInputBtn.rx.tap
            .flatMap { [unowned self] () -> Observable<(DemoOcrRecognizeResult)> in
                let DemoPdaOcrScanVC = DemoPdaOcrScanVC(isSetupBarCodeRecognize: true, isSetupDigitalRecognize: true)
                DemoPdaOcrScanVC.delegate = self
                self.navigationController?.pushViewController(DemoPdaOcrScanVC, animated: true)
                return DemoPdaOcrScanVC.pushInPhoneNumConfirmVC
            }
            .filter { !$0.barCodeStr.isEmpty }    // 当ocrRecognizeResultVariable发生变化 且 barCodeStr不为空时 => pushInPhoneNumConfirmVC
            .flatMap { [unowned self] (DemoOcrRecognizeResult) -> Observable<(Bool)>  in
                let DemoPhoneNumConfirmVC = DemoPhoneNumConfirmVC(DemoOcrRecognizeResult: DemoOcrRecognizeResult)
                DemoPhoneNumConfirmVC.delegate = self
                self.navigationController?.pushViewController(DemoPhoneNumConfirmVC, animated: true)
                return DemoPhoneNumConfirmVC.shouldPopBackToInputVC
            }
            .filter { $0 == true }    // 仅当 true == shouldPopBackToInputVC时 => popBackToInputVC
            .subscribe(onNext: { [unowned self] _ in
                _ = self.navigationController?.popToViewController(self, animated: true)
            })
            .disposed(by: disposeBag)

这样 A 跳 B 跳 C Pop To A 的逻辑都在 A 里面.

C pop to A 直接这样写就可以了 self.navigationController?.popToViewController(self, animated: true).

问题 1⃣️ B 跳 C 时需要传递值时怎么办? A怎么知道B要传什么值给C?

Observable<(DemoOcrRecognizeResult)

问题 2⃣️ C pop to A 时 C 的值怎样带回给 A 并触发 A 的刷新动作?

为了清晰 C pop back to A时 => 建议采用代理模式 来 刷新A

RxSwift介绍

limboy关于RxSwift的介绍写的很好,下面的内容摘取自 是时候学习 RxSwift了

是什么

在说 RxSwift 之前,先来说下 Rx, ReactiveX 是一种编程模型,最初由微软开发,结合了观察者模式、迭代器模式和函数式编程的精华,来更方便地处理异步数据流。其中最重要的一个概念是 Observable。

Object-C时代对应的是ReactiveCocoa. ReactiveCocoa是Github在制作Github客户端时开源的一个副产物,缩写为RAC。它是Objective-C语言下FRP思想的一个优秀实例,后续版本也支持了Swift语言。

Swift语言的推出为iOS界的函数式编程爱好者迎来了曙光。著名的FRP开源库Rx系列也新增了RxSwift,保持其接口与ReactiveX.net、RxJava、RxJS接口保持一致。

理解 Observable

举个简单的例子,当别人在跟你说话时,你就是那个观察者,别人就是那个 Observable,它有几个特点:

你在听到对方说的话后,也可以有几种反应:

RxSwift Workflow

大致分为这么几个阶段:先把 Native Object 变成 Observable,再通过 Observable 内置的各种强大的转换和组合能力变成新的 Observable,最后消费新的 Observable 的数据。

Trello
Native Object -> Observable

1⃣️ .rx extension

假设需要处理点击事件,正常的做法是给 Tap Gesture 添加一个 Target-Action,然后在那里实现具体的逻辑,这样的问题在于需要重新取名字,而且丢失了上下文。RxSwift (确切说是 RxCocoa) 给系统的诸多原生控件(包括像 URLSession)提供了 rx 扩展,所以点击的处理就变成了这样:

let tapBackground = UITapGestureRecognizer()

tapBackground.rx.event
    .subscribe(onNext: { [weak self] _ in
        self?.view.endEditing(true)
    })
    .addDisposableTo(disposeBag)

view.addGestureRecognizer(tapBackground)

2⃣️ Observable.create

通过这个方法,可以将 Native 的 object 包装成 Observable,比如对网络请求的封装:

public func response(_ request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
    return Observable.create { observer in
        let task = self.dataTaskWithRequest(request) { (data, response, error) in
            observer.on(.next(data, httpResponse))
            observer.on(.completed)
        }

        task.resume()

        return Disposables.create {
            task.cancel()
        }
    }
}

出于代码的简洁,略去了对 error 的处理,使用姿势类似:

let disposeBag = DisposeBag()

response(aRequest)
  .subscribe(onNext: { data in
    print(data)
  })
  .addDisposableTo(disposeBag)

这里有两个注意点:

实际项目中 我们可以在项目中的BaseVC中 统一声明一个disposeBag变量, 其他继承于BaseVC的使用了RxSwfit的ViewController 都可以把返回的Disposable 扔到这个disposeBag中.

DemoPdaBaseVC

class DemoPdaBaseVC: UIViewController {

    let disposeBag = DisposeBag()
  }

  // MARK: ViewController 四件套

DemoWayBillInputVC

class DemoWayBillInputVC: DemoPdaBaseVC {
}

extension DemoWayBillInputVC {
    func handleDataChange() {
        // 响应 viewModel.wayBillStrVariable 变化
        self.viewModel.wayBillStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .filter { !$0.isEmpty }
            .bind(to: self.wayBillTextField.rx.text)
            .disposed(by: disposeBag)

        // 响应 phoneNumStrVariable 变化
        self.viewModel.phoneNumStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .bind(to: self.phoneNumTextField.rx.text)
            .disposed(by: disposeBag)
    }
  }

3⃣️ Variable()

Variable(value) 可以把 value 变成一个 Observable,不过前提是使用新的赋值方式 aVariable.value = newValue,来看个 Demo

let magicNumber = 42

let magicNumberVariable = Variable(magicNumber)
magicNumberVariable.asObservable().subscribe(onNext: {
    print("magic number is \($0)")
})

magicNumberVariable.value = 73

// output
//
// magic number is 42
// magic number is 73

起初看到时,觉得还蛮神奇的,跟进去看了下,发现是通过 subject 来做的,大意是把 value 存到一个内部变量 _value 里,当调用 value 方法时,先更新 _value 值,然后调用内部的 _subject.on(.next(newValue)) 方法告知 subscriber。

4⃣️ Subject

Subject 简单来说是一个可以主动发射数据的 Observable,多了 onNext(value), onError(error), ‘onCompleted’ 方法,可谓全能型选手。

let disposeBag = DisposeBag()
let subject = PublishSubject<String>()

subject.addObserver("1").addDisposableTo(disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").addDisposableTo(disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

记得在 RAC 时代,subject 是一个不太推荐使用的功能,因为过于强大了,容易失控。RxSwift 里倒是没有太提及,但还是少用为佳。

Observable -> New Observable

Observable 的强大不仅在于它能实时更新 value,还在于它能被修改/过滤/组合等,这样就能随心所欲地构造自己想要的数据,还不用担心数据发生变化了却不知道的情况。

1⃣️ Combine

Combine 就是把多个 Observable 组合起来使用,比如 zip

zip 对应现实中的例子就是拉链,拉链需要两个元素这样才能拉上去,这里也一样,只有当两个 Observable 都有了新的值时,subscribe 才会被触发。

let stringSubject = PublishSubject<String>()
let intSubject = PublishSubject<Int>()

Observable.zip(stringSubject, intSubject) { stringElement, intElement in
    "\(stringElement) \(intElement)"
    }
    .subscribe(onNext: { print($0) })
    .addDisposableTo(disposeBag)

stringSubject.onNext("🅰️")
stringSubject.onNext("🅱️")

intSubject.onNext(1)
intSubject.onNext(2)

// output
//
// 🅰️ 1
// 🅱️ 2

如果这里 intSubject 始终没有执行 onNext,那么将不会有输出,就像拉链掉了一边的链子就拉不上了。

除了 zip,还有其他的 combine 的姿势,比如 combineLatest / switchLatest 等。

2⃣️ Transform

这是最常见的操作了,对一个 Observable 的数值做一些小改动,然后产出新的值,依旧是一个 Observable。

let disposeBag = DisposeBag()
Observable.of(1, 2, 3)
    .map { $0 * $0 }
    .subscribe(onNext: { print($0) })
    .addDisposableTo(disposeBag)

3⃣️ Filter

Filter 的作用是对 Observable 传过来的数据进行过滤,只有符合条件的才有资格被 subscribe。

extension DemoWayBillInputVC {
    func handleDataChange() {
        // 响应 viewModel.wayBillStrVariable 变化
        self.viewModel.wayBillStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .filter { !$0.isEmpty }
            .bind(to: self.wayBillTextField.rx.text)
            .disposed(by: disposeBag)

        // 响应 phoneNumStrVariable 变化
        self.viewModel.phoneNumStrVariable.asObservable()
            .observeOn(MainScheduler.instance)    // Tips: We should always handle data changes in UI with main thread.
            .bind(to: self.phoneNumTextField.rx.text)
            .disposed(by: disposeBag)
    }
}

其他

RxDataSource

Writing table and collection view data sources is tedious. There is a large number of delegate methods that need to be implemented for the simplest case possible.

观点: 目前有些复杂,不如delegate模式清晰.

参考资料 & 更多内容请参考:

是时候学习 RxSwift了

iOS开发下的函数响应式编程

The Right Way to Architect iOS App with Swift

上一篇下一篇

猜你喜欢

热点阅读