功能介绍:
点击下图重拍按钮,调取ocr,拍摄身份证主页,身份证副页,驾驶证主页,驾驶证副页(不调用ocr,单纯的拍照)。拍摄完成后显示在 UIImageView 上,对应的回调数据显示到相应UI上。
技术探索: 单向数据流动的MVVM
ViewModel 作为一个固定输入输出的状态机。给定它的当前状态以及输入,那么输出状态是明确且可预测的。比如,viewModel输入一个身份证主页的点击事件,我们经过
- 请求接口获取
ocr签名 - 拿着签名和身份证主页类型调取SDK进行拍摄(依赖第一步的签名模型得到证件信息)
- 将sdk回调的结果转成需要的模型
- 拍摄成功开始上传图片
最终我们向view输出的是 CardInformation(包含所有证件信息的模型)和各种证件的上传状态 CertificateUploadType(用于告诉view是否上传完成)
ViewModel 的输入
init(with input:(
ocrTaps: [Observable<Int>],
drivingLicenseBackTap: Signal<Data>?),
dependency service: OcrService)
ocrTaps: 一系列ocr证件的点击事件,关联值 Int 区分类型,写成数组灵活性高,而且后续还可以拓展其它ocr业务
drivingLicenseBackTap: 驾驶证副业,和其它类型不一样,点击副页,不调ocr,只上传图片。可传可不传,如果不传,会处理成 never。而且Data依赖于外部调用相机拍摄图片后生成,需要把点击事件+拍摄回调 封装成一个 Signal,遵守 OcrServiceProtocol 协议的 UIViewController 会自动获取生成方法。
ViewModel 的输出
typealias UploadResult = (uploadResult: CertificateUploadType, type: Int, image: UIImage)
let information: Driver<OcrService.CardInformation?>
let ocrUploadState: Driver<UploadResult>
let normalUploadState: Driver<UploadResult>
information: 各种ocr业务类型汇合成的一个总的模型,每一种业务类型通过调起SDK会从SDK的回调里得到一组证件信息,由于这些信息比较散乱,每一个业务回调信息都用字典接收,然后通过字典的 merging 操作汇成一个总的字典集合,然后把字典转成 OcrService.CardInformation 就可以了。
ocrUploadState: 每一项业务的上传状态,type 区分业务,image(SDK拍摄的图片) 用于展示
normalUploadState: 驾驶证背面拍摄信息
ViewModel事件的流转
init(with input:..., dependency:...) {
// 1.把每一次tap进来的不同类型的 `信号` 合并成一个 sequence
let actions = Observable.merge(input.ocrTaps)
// 2.把 service 的一系列事件处理成一个临时的 dataSource,dataSource 是一个
// SDK返回的证件信息的字典集合
let dataSource = actions
.filter { $0 > 0 && $0 < 4 } // type只接收 1、2、3
.flatMapLatest { type -> Observable<(Int, OcrSignModel)> in
// 请求接口获取ocr签名
return service.getOcrSign(sdkType: type).map { model in
(type, model)
}
}.flatMapLatest { tuple -> Observable<(data: [String: Any], type: Int, image: Data)> in
// 调起SDK拍摄证件
return service.ocrSign(with: tuple.0, mode: tuple.1)
// 需要catchError,不然SDK出错后,事件会断掉
.catchErrorJustReturn((data: [:], type: tuple.0, image: Data()))
}.asDriver(onErrorJustReturn: (data: [:], type: 1, image: Data()))
// 一个临时变量,用于合并每次识别后的结果字典
let informationDict = BehaviorRelay<[String: Any]>(value: [:])
// 每次处理完拍摄流程,将字典与临时字典集合合并,并转为struct模型
information = dataSource.asObservable()
.map { $0.data }
.asDriver(onErrorJustReturn: [:])
.flatMapLatest { dict in
let oldValue = informationDict.value
let newValue = dict.merging(oldValue) { first, _ in first }
informationDict.accept(newValue)
return service.transformDictToModel(with: newValue)
.asDriver(onErrorJustReturn: nil)
}
// 图片上传的状态信息,用于loadding显示
ocrUploadState = dataSource.flatMapLatest { ... }
normalUploadState = drivingLicenseBackTap.flatMapLatest { ... }
}
事件的流转依赖于flatmapLatest 操作符,本质上就是把一个基础信号,经过一系列加工,并把副作用所产生的值进行保留,处理,最终转换成可以直接绑定到 UI 上的数据。
service 内部
func getOcrSign(sdkType:Int) -> Observable<OcrSignModel> {
return CertificateAPI.networking.wsRequest(.getOcrSign)
.decodeResult(OcrSignModel.self)
.asObservable()
.flatMapLatest { result -> Observable<OcrSignModel> in
return Observable<OcrSignModel>.create { observer in
switch result {
case let .sucessed(model, _):
observer.onNext(model)
// onNext 执行后要手动 结束序列
// 不然序列会一直等待,导致loading 不结束
observer.onCompleted()
case let .failed(error, _):
observer.onError(error)
}
return Disposables.create()
}
}
}
func ocrSign(with type: Int, mode: OcrSignModel) -> Observable<(data: [String: Any], type: Int, image: Data)> {
...
return Observable<(data: [String: Any], type: Int, image: Data)>
.create { observer in
ocrService.start...
func transformDictToModel(with dict: [String: Any]) -> Observable<CardInformation?> {
guard let data = try? JSONSerialization.data(withJSONObject: dict) else {
return Observable<CardInformation?>.just(nil)
}
let decorder = JSONDecoder()
let model = try? decorder.decode(CardInformation.self, from: data)
return Observable<CardInformation?>.just(model)
}
service 内的每一步操作都可以转成 Observable,甚至把字典转成模型的方法,也可以这么搞。而我们之前比如 请求 ocr签名,调取 SDK 等操作就完全被当做 Observable 的副作用来执行。
这次再回到 ViewModel 中回顾一下简化后的代码:
information = Observable.merge(input.ocrTaps) // 将一系列ocr点击事件合并
.filter { ... } // 过滤掉不符合的tap类型
.flatMapLatest { service.getOcrSign... } // 获取签名
.flatMapLatest { service.ocrSign... } // 拿到签名调起SDK
.map { $0.data } // 取出sdk返回的字典
.flatMapLatest { service.transformDictToModel... } // 字典转成最终的模型
怎么样,经过一系列的事件流转,我们的代码是不是变得更加直观和易读了?而且发现了吗,我们的viewModel 中再也没有那种 BehaviorRelay的 存储属性了,输入和输出变得纯净和易维护。(当然了,前提还是大家要深入了解flatMapLatest 等操作符的工作机制)
ViewModel 的拓展性
OcrServiceVieModel 可以用来继承,我们可以依赖于父类的几个output属性在子类里增加更多的output,例如生成一个Bool类型的 loading 直接用于 UIActivityIndicatorView 的绑定:
class OcrServiceQueryViewModel: OcrServiceVieModel {
var rightLoading: Driver<Bool> {
normalUploadState
.map { $0.uploadResult }
.map { result in
if case .uploading = result {
return true
}
return false
}
}
}
/* 下面是vc中的部分代码 */
class InformationQueryController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let ocrVM = OcrServiceQueryViewModel(
with: (ocrTaps: [frontClicked], drivingLicenseBackTap: backClicked),
dependency: OcrService.init())
ocrVM.information
.compactMap { $0 }
.drive(contentView.rx.remakeDataSource)
.disposed(by: bag)
ocrVM.rightLoading
.drive(rightActivityIndicator.rx.isAnimating)
.disposed(by: bag)
}
}
另一个小细节 viewController 中不在需要持有 viewModel 了。viewModel 作为一个临时变量只在 viewDidLoad 中处理输入输出,从根本上避免了 viewModel 作为一个全局变量,在vc中被滥用的行为。
也许你会问,viewModel 的input 如果是一个 cell 的点击事件怎么办?
好办...
let didSelectedItem = collectionView.rx.itemSelected.asSignal()
其它的情况就不列举了,具体情况具体分析,不管是什么UI控件,也不管是什么UI事件,我们总有办法把他转换成一个 序列 的。
写在最后
如果你也对RxSwift 感兴趣,欢迎私信交流~