首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

MVVM+RxSwift在iOS端千帆外勤项目上的研究与实践

目录

01

MVVM 架构

02

响应式编程

03

网络层重构

·让统一网络请求接口并返回一个可观察的序列(Observable)

·Plugin 的用法和作用

·Result以及错误处理

04

基于网络层及RxCocoa实现MVVM

·RxCocoa

·ViewModel

·ViewController

05

最后

06

参考资料

MVVM架构

(本段摘自《App架构》)

模型-视图-视图模型 (MVVM) 是一种基于MVC进行改进的模式,它将所有模型相关的任务 (包括更新模型,观察变更,将模型变形为可以显示的形式等) 从控制器层抽离出来,放到新的叫做视图模型的一层对象中。在通常的iOS实现中,视图模型位于模型和视图控制器之间:

和所有好的模式一样,MVVM 所做的不仅仅是把代码移动到新的地方。加入一层新的视图模型层的目的是双重的:

01

鼓励将模型和视图之间的关系构建为一系列的变形管道。

02

提供一套独立于app框架的接口,但是它在相当程度上代表了视图应该展示的状态。

响应式编程

在MVVM架构中,通常会引入绑定来完成视图和模型的同步,而在iOS中通常会采用响应式编程来实现绑定。在iOS端常用的响应式框架主要有以下两个:

01

ReactiveCocoa(RAC),在Objective-C中RAC长期是响应式编程框架的不二选择,ReactiveSwift是RAC的Swift版本;

02

RxSwift是Rx社区编写的Rx的Swift版本。

RAC和RxSwift都是特别好的响应式框架,然而考虑到千帆项目都是使用Swift进行开发的,所以我们选用了RxSwift作为响应式编程的方案。

网络层重构

为了结合RxSwift实现MVVM架构,我们将网络层做了适应性重构,让网络层能够具备响应式并且统一网络层对外暴露的接口,同时我们参考了moya框架的设计思路,给网络层添加了Plugin选项,此外我们还将网络层的错误处理方案进行了相关优化。

让统一网络请求接口并返回一个可观察的序列(Observable)

func request(_ router: URLRequestConvertible, _ plugins: [Plugin] = [TokenPlugin()]) -> Observable> { plugins.forEach { $0.willSendRequest(router) } return Observable.create { (observer) -> Disposable in Alamofire.request(router).responseJSON(completionHandler: { (resp: DataResponse) in plugins.forEach { $0.didReceiverResponse(resp) } switch resp.result { case let .failure(error): observer.onNext(Result .failure(NetworkError.alert(resp.response?.statusCode ?? -1))) case let .success(value): if let json = value as? [String: Any] { if let resp = Mapper().map(JSON: json) { if resp.success { observer.onNext(Result.success(resp)) } else { observer.onNext(Result.failure( NetworkError.toast(resp.message.first ?? "无法访问服务器"))) } } } } }) return Disposables.create() } }

以上代码块为网络层的主体,request方法是网络层提供给外部调用的接口,接口的router参数保持了原来的架构中的对Alamorfire请求封装的参数,而Plugin参数则为网络层可选配置提供了可扩展接口,Plugin的概念参考自moya框架,request方法返回了一个可观察序列Observable>, 这样做的好处是我们不用在每个调用网络请求的代码中再去创建可观察序列,而只需将网络层返回的数据进行数据转换就可以与UI进行绑定。

Plugin的用法和作用

protocol Plugin { func willSendRequest(_ request: URLRequestConvertible) func didReceiverResponse(_ response: DataResponse?)}

Plugin是一个接口,它定义了两个接口方法:willSendRequest 在即将发送网络请求时被调用,didReceiverResponse在网络请求返回后调用,我们可以通过实现Plugin接口来实现一些功能, 比如,我们可以定义一个LoggerPlugin实现Plugin接口,并在willSendRequest中打印网络请求的参数和地址的请求信息,在didReceiverResponse中打印网络请求返回的数据,这样做的好处是把与请求主体不相关的功能从网络层抽离出来,方便扩展。

Result以及错误处理

enum Result { case success(Value) case failure(NetworkError)}

Observable有一个特征: 当Observable产生error信号,整个序列将终止。针对Observable的这个特点,有两个方案处理error信号:

01

利用RxSwift提供的错误信号处理函数 catchError,提供一个指定的数据来处理错误的情况。

02

利用Result数据结构的特性,将错误信号转换为普通的信号传递,并在合适的时机对Result进行解封。

很显然,第一种方案需要针对不同的网络接口提供不同的指定数据,虽然这是应该的,但是这样的方式无疑会增加我们的代码量,甚至是指定数据的处理也会给正常的数据处理添加更多的业务逻辑,这不是我们希望看到的,第二种方案的关键其实将数据从Result中解包出来,我们可以定义一个统一的函数来处理:

extension ObservableType { func unwrapper(_ errorHandler: ((NetworkError)->())? = nil) -> Observable { return self.map { (element) -> NetResponse? in if let result = element as? Result { switch result { case .success(let value): return value case .failure(let error): if let handler = errorHandler { handler(error) } print(error.message) return nil } } return nil } }}

因为服务端返回的数据通常是具有统一格式的,所以我们可以通过统一的错误处理方法去解包Result,这样的好处是将错误逻辑从网络请求中抽离出来,让调用者关注的业务逻辑更少。为了处理一些特殊的情况,在解包函数unwrapper中加入了一个可选闭包,当调用者需要自定义错误处理逻辑的时候,就可以通过传入错误处理闭包来替换掉统一的错误处理方案。

基于网络层以及RxCocoa实现MVVM

RxCocoa

RxCocoa是Rx社区实现的Cocoa框架的rx扩展,通过这个框架,许多的UI事件可以被转化为事件流,视图属性可以被转化为可绑定的观察者,例如:

///ViewController中nameTextField.rx.text .orEmpty .asDriver() .drive(viewModel.name)///ViewModel中var name: Variable = Variable("")

在我们自定义的视图中,我们需要自定义类似RxCocoa提供我们的属性,这时候就需要扩展Reactive,例如:

extension Reactive where Base: KeyBoard { ///定义相关属性或事件 }

ViewModel

ViewModel在编译期间不包含对 view 或者 controller 的引用。它暴露出一系列属性,用来描述每个view在显示时应有的值,视图展示的数据都应该由ViewModel持有,并在ViewController中绑定到View上。

///登录模块中LoginViewModel的主体var name: Variable = Variable("")var password: Variable = Variable("") init() {} func login() -> Observable { return Networking.default .request(AuthRouter.login(name: self.name.value, password: self.password.value)) .unwrapper() .mapObject(type: User.self) }}

其中name,password即为与登录界面中可供用户输入的账号和密码的text绑定的序列,login方法负责用户点击登录按钮时向网络层发送消息,并将网络层返回的数据进行处理返回给视图控制器。

ViewController

视图控制器在MVVM架构中只作为View的一部分,负责绑定ViewModel和View,并将View需要展示的数据(ViewModel中的序列)与View绑定起来,ViewController中不存在任何View State和Model State。

///视图控制器的主体代码loginButton.rx.tap.asObservable() .throttle(0.3, scheduler: MainScheduler.instance) .flatMapLatest .doOnNext { _ in self.performSegue(withIdentifier: "loginSegue", sender: nil)} .subscribe().disposed(by: disposeBag)nameTextField.rx.text .orEmpty.asDriver().drive(viewModel.name) .disposed(by: disposeBag)passwordTextField.rx.text .orEmpty.asObservable() .bind(to: viewModel.password) .disposed(by: disposeBag)

其中throttle,flatMapLatest都是rx的操作符,这些操作符也是帮助实现部分需求的关键,subscribe()是ObservableType定义的订阅函数。subscribe()函数的实现代码在ObservableType + Extensions中,subscribe()函数内部创建了一个观察者observer,并调用了ObservableType的subscribe(_ observer: O)方法,即用一个观察者订阅了Observable序列。

在Observable被创建的闭包中可以看到信号的发出是通过observer的相关方法发出的,因此若是Observable未被订阅,那么observer其实是不存在的,信号自然不会被发出。所以每个可观察序列都应该被订阅,才能发出信号。disposed函数是管理rx资源的函数,在可观察序列被订阅后通常会调用,disposeBag则是被视图控制器持有的,是为了让rx资源的生命周期与视图控制器一致。

最后

架构不是银弹,架构的目的是加快我们的开发效率,提高我们的代码质量;如果当架构的引入没有达到它最初的目标,还带来了不低的学习成本就得不偿失了。

我们之所以选择重构的app架构,是因为我们在开发过程中确实感觉到当前的MVP架构不如Android的MVVM开发效率快同时随着模块的增加,外勤的代码逻辑太过于混乱,并且在与Android的同学讨论了以后决定参考Android的架构模式,在iOS中引入MVVM架构。

参考资料

https://github.com/ReactiveX/RxSwift

https://beeth0ven.github.io/RxSwift-Chinese-Documentation/

http://rxmarbles.com

https://objccn.io/products/app-architecture

https://github.com/Moya/Moya

关注我们

公众号ID:headingfun

有料,有爱,有梦想。

*本文中的完整代码已提交至Github

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180719G1DA0S00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券