一个Demo体验RxSwift+MVVM 的最佳实践

643 阅读4分钟

功能介绍:

点击下图重拍按钮,调取ocr,拍摄身份证主页,身份证副页,驾驶证主页,驾驶证副页(不调用ocr,单纯的拍照)。拍摄完成后显示在 UIImageView 上,对应的回调数据显示到相应UI上。

技术探索: 单向数据流动的MVVM

ViewModel 作为一个固定输入输出的状态机。给定它的当前状态以及输入,那么输出状态是明确且可预测的。比如,viewModel输入一个身份证主页的点击事件,我们经过

  1. 请求接口获取ocr签名
  2. 拿着签名和身份证主页类型调取SDK进行拍摄(依赖第一步的签名模型得到证件信息)
  3. 将sdk回调的结果转成需要的模型
  4. 拍摄成功开始上传图片

最终我们向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 中不在需要持有 viewModelviewModel 作为一个临时变量只在 viewDidLoad 中处理输入输出,从根本上避免了 viewModel 作为一个全局变量,在vc中被滥用的行为

也许你会问,viewModelinput 如果是一个 cell 的点击事件怎么办? 好办...

let didSelectedItem = collectionView.rx.itemSelected.asSignal()

其它的情况就不列举了,具体情况具体分析,不管是什么UI控件,也不管是什么UI事件,我们总有办法把他转换成一个 序列 的。

写在最后

如果你也对RxSwift 感兴趣,欢迎私信交流~