介绍
此篇文章仅献给想学习响应式编程的初学者。
此demo是一个完整的登录功能的实现,包含第三方:
- RxSwift
- RxCocoa
- Moya
- SwiftJson
- ObjectMapper
- Alamofire
实现了登录页面的输入校验,http设置及请求,模型转换,缓存,自定义错误处理插件
关于请求缓存的举例在登录中不太恰当,只是用来说明如何实现
demo结构介绍
- DMUser: 用户模型
- DMLoginViewController: 登录控制器
- DMLoginResult:登录状态(枚举)
- DMLoginViewModel:数据业务处理
- DMLoginProtocols: 登录所用到的接口(协议)
- DMLoginService:服务(具体类,实现了所定义的协议接口)
- DMLoginHttp:遵从Moya的TargetType,请求定义
- DMDeployProvider:提供请求的额外配置,包含:request和session manager
- DMMoyaHttpErrorHandlePlugin: 自定义插件,可用来统一处理错误
- DMCache: 缓存,并扩展RxMoya支持缓存
数据校验
关于登录的输入校验,直接使用RXSwift官方demo内的GitHubSignup的UsingDriver > 2,看过的可以跳过该章节。
首先介绍下driver,
关于driver和observable的区别,官方是这样说的:
* Can't error out (不能错误终止信号)
* Observe on main scheduler (在主线程观察)
* Subscribe on main scheduler (在主线程订阅)
* Sharing side effects (分享副作用,shareReplay())
官方示例中 UsingDriver > 2 是UsingVanillaObservables > 1的优化版本
介绍下官方示例
官方示例采用的是MVVM + OOP的方案实现。
在控制器中,将输入源(文本框,按钮等事件)和实现协议的服务类实例传递给viewmodel中,然后在viewmodel中去观察输入源的变化并处理,在控制器中去订阅这些信号(处理过的信号)。流程大体是这样的,具体参考代码,这里重点介绍两处:
关于数据的传递,也可以称为数据的流向。
由于swift闭包的特性,闭包内参数未显示声明参数的类型,或者直接省略了参数列表,而已$0, $1代替,造成了理解上的不方便。
//viewmodel中校验用户名
validateUserName = input.userName.flatMapLatest {
return validService.validationUserName($0)
.asDriver(onErrorJustReturn: .failed(message: "链接服务失败"))
}
//校验方法声明
func validationUserName(_ usernName: String) -> Observable
//校验请求方法声明
func checkUserNameAvaliable(userName: String) -> Observable
//控制器中的UI绑定
viewModel.validateUserName
.drive(userNameValidLabel.rx.validationResult)
.addDisposableTo(disposeBag)
在这一个流程中,数据是如何传递与使用的呢?
初学者刚开始接触可能难免困惑,这一点要提及下函数式编程几个概念(Functor,Applicative,Monad),有兴趣的可以阅读下,有助于理解。
个人理解:将一个值a封装为上下文值A,运用一个处理普通值但返回上下文中的值的函数B,得到另一个上下文中的值C,然后不断传递再处理,就形成了响应式编程实现的这种独特写法,还可以参考:
雷纯锋的技术博客1
雷纯锋的技术博客2
解析上方代码片段:
- 在viewmodel中去观察文本框的变化,flatMapLatest是获取最新的舍弃旧的,该闭包内省略的参数是文本框的文本,即textField.text <=> $0,将文本传递给校验方法;
- 在校验方法中,完成对该文本的校验,并返回一个可观察的对象(不知道怎么称呼,就以单词意思解释了)Observable
,是对ValidationResult进行了封装(可以理解讲一个普通值变为了一个上下文中的值,方便链式调用),在该方法中又调用了校验请求的方法; - 在校验请求的方法中,实际发起了网路请求,并对网络请求结果进行处理,返回Observable
这样的值(可以理解为将一个普通的bool类型的值封装为了一个上下文中的值),在校验方法中又将该
Observable转化为了Observable ; - 回到viewmodel中,validService.validationUserName()这个方法就是拿到上一步的处理结果Observable
,而driver是特殊的observable,只要满足条件就可以转化为driver,但由于observable可以出现错误终止信号,而driver不可以,所以driver捕获该错误将之转化为发送其它正常信号; - 控制器中viewModel.validateUserName.drive拿到了之前的处理结果Observable
,接下来就是如何将结果具体绑定到UI上。
处理结果与UI的绑定
extension Reactive where Base: UILabel {
var validationResult: UIBindingObserver {
return UIBindingObserver(UIElement: base) { label, result in
label.textColor = result.textColor
label.text = result.description
}
}
}
对于RxCocoa支持的可以直接使用,但不支持的或者需要自定义的就要实现这个方法,此处对于Reactive进行扩展,实现了label对于ValidationResult的自定义展示。
网络请求
网络请求采用的是比Alamofire更高一层的抽象,Moya,并且也支持了RxSwift。
基本用法不多介绍,参照demo(DMLoginHttp,DMLoginService)或其它博客,这里介绍下对它的其它的一些自定义。
首先介绍下它的初始化参数,都有默认值,可以省略,但业务总是复杂多变的,需要我们的定制,所以先了解下它的各项参数含义以便配置。可见官方文档 Providers.md
public init(
endpointClosure: @escaping (Target) -> Moya.Endpoint = default,
requestClosure: @escaping (Moya.Endpoint, @escaping Moya.MoyaProvider.RequestResultClosure) -> Swift.Void = default,
stubClosure: @escaping (Target) -> Moya.StubBehavior = default,
manager: Moya.Manager = default,
plugins: [PluginType] = default,
trackInflights: Bool = default)
- Moya的方式是:endPoint -> request,所以在此处可以对请求进行再设置,包含url,header等;
- 接收之前的 endPoint生成request的实例,在此可对request再设置;
- 用来控制模拟请求的,即返回的是本地数据;
- 用来生成Alamofire的sessionManager实例,此处可进行超时时间,https自签名证书的设置等,假如你之前直接使用Alamofire的manager的设置都可以移植到此处设置,;
- 插件设置,本身提供了三种插件(NetworkActivityPlugin,CredentialsPlugin,NetworkLoggerPlugin),还可以自定义实现插件,只需实现插件协议
- trackInflights ??(额暂时不知道,有知道的可以告诉我下)
此demo我自定义了一下相关的:
- DMDeployProvider:定义了endPoint和manager
- DMMoyaHttpErrorHandlePlugin: 自定义插件,错误统一处理
缓存
由于Moya未提供离线缓存,因此需要自己扩展Moya实现缓存。此例子中缓存是以字典形式存在内存中的,可替换为真正的缓存框架,此处仅仅是为了展示如何扩展缓存。
DMCache.swift中,
定义了缓存策略:DMCacheType
缓存类(假的,示范用): DMCache
扩展:是Moya支持缓存,tryCache()
核心代码:
return Observable.create({[weak self, weak cache] observe -> Disposable in
switch cacheType {
case .onlyCache:
print("使用缓存!!!")
if let response = cache?.getValue(forKey: identifier) {
observe.onNext(response)
}
observe.onCompleted()
case .cacheThenRequest:
print("先使用缓存再请求数据!!!")
if let response = cache?.getValue(forKey: identifier) {
observe.onNext(response)
}
fallthrough
case .onlyRequest:
print("请求数据!!!")
task = self?.request(target) { result in
switch result {
case let .success(response):
observe.onNext(response)
observe.onCompleted()
cache?.storeValue(response, forKey: identifier)
case let .failure(error):
observe.onError(error)
}
}
}
return Disposables.create {
task?.cancel()
}
})
此处仅需要依据不同的缓存策略,包装实现即可。
注意fallthrough关键字,小心坑~~ :)
模型
最后拿到数据,使用ObjectMapper进行JSON转模型即可,不多介绍
总结:此demo示例了如何用响应式编程将众多功能结合在一起,着重介绍了RxSwift和Moya的结合使用,后续会重点关注RxSwift的实现原理及全方位讲解使用。
看~灰机灰过来了~灰机又灰过去了~