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

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,它有几个特点:

  • 可能会不断地跟你说话。(onNext:)

  • 可能会说错话。(onError:)

  • 结束会说话。(onCompleted)

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

  • 根据说的话,做相应的事,比如对方让你借本书给他。(subscribe)

  • 把对方说的话,加工下再传达给其他人,比如对方说小张好像不太舒服,你传达给其他人时就变成了小张失恋了。(map:)

  • 参考其他人说的话再做处理,比如 A 说某家店很好吃,B 说某家店一般般,你需要结合两个人的意见再做定夺。(zip:)

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)

这里有两个注意点:

  • Observerable 返回的是一个 Disposable,表示「可扔掉」的,扔哪里呢,就扔到刚刚创建的袋子里,这样当袋子被回收(dealloc)时,会顺便执行一下 Disposable.dispose(),之前创建 Disposable 时申请的资源就会被一并释放掉。

实际项目中 我们可以在项目中的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)
    }
  }
  • 如果有多个 subscriber 来 subscribe response(aRequest) 那么会创建多个请求,从代码也可以看得出来,来一个 observer 就创建一个 task,然后执行。这很有可能不是我们想要的,如何让多个 subscriber 共享一个结果,这个后面会提到。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容