现状
目前项目采用的是 MVC
结构,UIViewController
包含大量代码,包括网络请求,数据处理,布局代码等,难于测试(虽然也不写测试)。随着项目发展,这部分内容会越来越多,越来越复杂,导致 UIViewController
中的代码越来越难维护。为了项目的可持续发展,决定开始开始使用 RxSwift
和 MVVM
。
ViewModel
ViewModel
扮演的是处理业务逻辑的角色,负责处理数据和为 ViewController
提供数据源。一般来说有几下几点要求:
- 可测试的,这样可以直接为
ViewModel
编写单元测试。 - 不知道
ViewController
或者View
的存在,只负责自己处理数据。 - 团队编写
ViewModel
时需要严格按照现有规范来编写。 - 配合
RxSwift
食用更佳
如何将 ViewModel 与数据源绑定
一般来说 ViewModel
与数据源绑定,指的是如何将用户的操作,网络状态绑定到 ViewModel
上,再将 ViewModel
的数据源绑定到 UI
上。
第一种绑定方式— protocol 与转换
首先定义一个 ViewModelType
协议,协议中包含 Input
,Output
和一个将 Input
转换为 Output
的方法。 RxSwift + MVVM: how to feed ViewModels 的 First approach — without Subjects 和 CleanArchitectureRxSwift/ViewModelType.swift at 55c852bbd7b4e5f9ee30426e2acdb7b038e848be · sergdort/CleanArchitectureRxSwift · GitHub 都是采用了这种写法。
这种写法,一看之下非常好用,也很干净,没有用到 Subject
,所有的处理都经由 transform
方法完成,但是有些情况下并不适用, 可以看到转换的机会只有一次,你必须在一开始就获取到所有 input
的数据源,通过 transform
方法将 input
转化为 output
。How to feed ViewModels 这篇文章里面也有提到,有些时候我们并不能一次过获取到所有的数据源,不同的数据源需要分开获取,这就需要使用第二种绑定方式。
第二种绑定方式 — 将 Input 和 Output 抽出来
在某些情况下,可能需要分开获取不同的数据源,这时候就需要借用 Subject
,但是我们可以不将 Subject
暴露出来。具体写法参考:RxSwift + MVVM: how to feed ViewModels 的 Second approach — with Subjects 。
Subject
属性是私有的,它为 input
提供了绑定来源,结构体 Input
有多少个属性就需要提供多少个 Subject
。同时 Subject
也可以作为 output
的数据来源,它是 input
和 output
两者之间的数据桥梁。
KickStarter 也是通过类似的方法将 Subject
隐藏起来,只不过 KickStarter 使用的是 ReactiveCocoa
,而且是通过方法来进行更新 Property
(Subject),而不是通过 Struct
来进行转换。 在 vm-structure.md 中有提到所有 inputs
都提供对应的 MutableProperty
(Property
相当于 RxSwift
的 Subject
),然后在 inputs
函数中更新对应的 Property
。
是否使用 Subject
在任何情况下,ViewModel
在处理数据时都是单向流动的,即 Input
接受数据输入,Output
输出处理后的数据,上面两种写法都满足这个要求。但是如果 Output
里包含了 Subject
,ViewController
就可以通过调用 Ouput
相关 Subject
的 onNext
方法来达到输出数据的目的,这样数据流动就没有经过 ViewModel
进行处理了,这显然违背了 ViewModel
的设计原则,所以如果要使用 Subject
,那对应的属性则应该是私有的。
Cell,View 等控件是否有需要添加 ViewModel
如果 Cell
,View
等 UI 控件的逻辑比较简单,可以先不添加 ViewModel
,对于比较复杂的 Cell
和 View
等 UI 控件,可以考虑添加 ViewModel
。
Network 层与 JSON 转换
现有项目使用的 Moya 来组织网络层,使用 ObjectMapper 来进行 JSON
和 Model
层的转换。 由于要将不同类别的接口分到不同的 enum
类中,创建一个 MultiMoyaProdider
类,可执行 TargetType
的请求。
class MultiMoyaProvider: MoyaProvider<MultiTarget> {
typealias Target = MultiTarget
init() {
let plugins: [PluginType]
#if DEBUG
plugins = [NetworkLoggerPlugin(verbose: true)]
#else
plugins = []
#endif
let endpointClosure = { (target: Target) -> Endpoint in
let endpoint: Endpoint =
Endpoint(url: target.baseURL.absoluteString + target.path,
sampleResponseClosure: {
.networkResponse(200, target.sampleData)},
method: target.method,
task: target.task,
httpHeaderFields: target.headers)
return endpoint
}
super.init(endpointClosure: endpointClosure, plugins: plugins)
}
@discardableResult
func multiRequest<T: TargetType>(_ target: T, completion: @escaping Completion) -> Cancellable {
return request(MultiTarget(target), completion: completion)
}
}
有了 MultiMoyaProvider
后可以进行 Rx
化的改造,Moya
本身就提供了对于 RxSwift
的支持:Moya/RxSwift.md at master · Moya/Moya · GitHub。 通过 MultiMoyaProvider
,可以提供三种方法来返回 Observable
。
typealias JSONDictionary = [String: Any]
// 1. 直接返回 Observable<JSONDictionary>
func jsonObservable(target: TargetType) -> Observable<JSONDictionary> {
return Observable.create { observer in
return provider.rx.request(MultiTarget(target))
.subscribe(onSuccess: { (response) in
// 验证 response 格式
if let error = valid(response) {
observer.onError(error)
return
}
guard let dictionary = try? response.mapJSON() as? JSONDictionary else {
observer.onError(MoyaError.jsonMapping(response))
return
}
observer.onNext(dictionary)
},
onError: { (error) in
observer.onError(error)
})
}
}
// 2. 返回单个 Model 类型
func objectObservable<T: Mappable>(target: TargetType, objectType: T.Type) -> Observable<T> {
return Observable.create { observer in
return provider.rx.request(MultiTarget(target))
.subscribe(onSuccess: { (response) in
if let error = valid(response) {
observer.onError(error)
return
}
guard let object = try? response.mapObject(objectType.self) else {
observer.onError(MoyaError.jsonMapping(response))
return
}
observer.onNext(object)
},
onError: { (error) in
observer.onError(error)
})
}
}
// 3. 返回 Model 数组
func objectArrayObservable<T: Mappable>(target: TargetType, objectType: T.Type) -> Observable<[T]> {
return Observable.create { observer in
return provider.rx.request(MultiTarget(target))
.subscribe(onSuccess: { (response) in
if let error = valid(response) {
observer.onError(error)
return
}
guard let objectArray = try? response.mapArray(objectType.self) else {
observer.onError(MoyaError.jsonMapping(response))
return
}
observer.onNext(objectArray)
},
onError: { (error) in
observer.onError(error)
})
}
}
为了方便 Response
与 Model
间的转换,给 Response
新增两个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension Response {
/// Maps data received from the signal into an object which implements the Mappable protocol.
/// If the conversion fails, the signal errors.
func mapObject<T: BaseMappable>(_ type: T.Type, context: MapContext? = nil) throws -> T {
guard let object = Mapper<T>(context: context).map(JSONObject: try mapJSON()) else {
throw MoyaError.jsonMapping(self)
}
return object
}
/// Maps data received from the signal into an array of objects which implement the Mappable
/// protocol.
/// If the conversion fails, the signal errors.
func mapArray<T: BaseMappable>(_ type: T.Type, context: MapContext? = nil) throws -> [T] {
guard let array = try mapJSON() as? [[String: Any]] else {
throw MoyaError.jsonMapping(self)
}
return Mapper<T>(context: context).mapArray(JSONArray: array)
}
}
然后在 ViewModel
具体的 input
中通过 flatMap
来调用网络请求:
1
2
3
4
5
input.flatMap {
jsonObservable...
}
.subscribe
...
相关资源
- http://community.rxswift.org/
- https://github.com/kickstarter/native-docs/blob/master/vm-structure.md
- native-docs/inputs-outputs.md at master · kickstarter/native-docs · GitHub KickStarter 重度使用
MVVM
和ViewModel
,上文阐述了他们关于ViewModel
的设计理念。 - RxSwift 中文文档 · RxSwift 中文文档
- http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx
- RxSwift + MVVM: how to feed ViewModels
- GitHub - sergdort/CleanArchitectureRxSwift: Example of Clean Architecture of iOS app using RxSwift
- 是时候学习 RxSwift 了 - Limboy’s HQ
Comments powered by Disqus.