使用RxSwift+Moya+ObjectMapper接入模型

一般情况下,从业务方从API中请求JSON数据时,一般都会经过以下三步:

1
2
--------1------------2----------
原始数据 -> JSON/字典 -> Model

当然,大部分情况下,原始数据就是JSON,所以第一步基本上只是对接受数据的一个类型转换。一般在网络层中,由组件方提供1步骤,而业务方往往在网络组件的回调中提供步骤2。简单的转换逻辑明了了,接下来就可以试下用Moya实现步骤1,ObjectMapper实现步骤2。

在结合RxSwift+Moya+ObjectMapper三者之后,常规JSON数据的获取与解析变得更加精简。以近期编写的一个V2ex API为例,获取个人信息接口如下:

1
2
3
4
5
6
7
func fetchMemberInfo(_ username: String? = V2exAppContext.shared.currentUsername,
_ id: Int? = nil) -> Observable<V2exMember> {
return V2exProvider
.request(.ShowMembers(username: username, id: id))
.mapObject()
.shareReplay(1)
}

嗯,没错,最终的调用就是这么简单明了!

实现

那么,上述函数的内部是如何实现的呢?

首先说下Moya。Moya是针对网络的一层封装,并且Moya在较为后期的版本中,还提供了RxSwift以及ReactiveCocoa的接口。针对RxSwift,Moya提供了以下两个好用的扩展:

1
2
open func request(_ token: Target) -> Observable<Response>
public func mapJSON(failsOnEmptyData: Bool = true) -> Observable<Any>

前者用来请求原始数据,后者则将原始数据转化成json。当然,Moya还提供了其他Rx扩展,比如filterStatus系列方法,这里就不展开了。

有了上面两个方法,业务方请求数据时,就可以这样调用:

1
2
3
4
5
let V2exProvider = RxMoyaProvider<V2ex>()

let json = V2exProvider
.request(.ShowMembers(username: username, id: nil))
.mapJSON()

上面json即为解析完成的JSON数据流。

得到JSON数据流之后,就可以执行步骤2了,这里选用的是ObjectMapper。ObjectMapper是一个Swift编写的模型<->JSON转换库,应用代码非常简单,只要模型遵守Mappable协议,并且实现对应的方法就可以了:

1
2
init?(map: Map)
mutating func mapping(map: Map)

然后在模型中设置对应属性的值,这里以V2ex的Member为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct V2exMember: Mappable {
var status: String?
var id: Int?
var url: String?
var username: String?
var website: String?
var twitter: String?
var psn: String?
var github: String?
var btc: String?
var location: String?
var tagline: String?
var bio: String?
var created: Int?
var avatar: V2exAvatar?

init?(map: Map) {

}

mutating func mapping(map: Map) {
status <- map["status"]
id <- map["id"]
url <- map["url"]
username <- map["username"]
website <- map["username"]
twitter <- map["twitter"]
psn <- map["psn"]
github <- map["github"]
btc <- map["btc"]
location <- map["location"]
tagline <- map["tagline"]
bio <- map["bio"]
created <- map["created"]
avatar = V2exAvatar(JSON: map.JSON)
}
}

可以看到,对于struct类型的模型,这种转换方式还是很优雅的。生成模型的话,也只需要很简单的代码就可以完成:

1
Mapper<V2exMember>().map(JSONObject: $0)

到这里为止,步骤2也完成了,接下就可以将步骤1、2连接起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
extension ObservableType where E == Response {
public func mapObject<T: Mappable>() -> RxSwift.Observable<T> {
return mapJSON()
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .background))
.flatMap {
return Mapper<T>().map(JSONObject: $0)
.flatMap{ Observable.just($0) } ??
Observable.error(NSError(domain: "V2ex",
code: -1,
userInfo: ["Error" : "failed to map object"]))
}
.observeOn(MainScheduler.instance)
}

public func mapObjectArray<T: Mappable>() -> RxSwift.Observable<[T]> {
return mapJSON()
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .background))
.flatMap { array -> Observable<[T]> in
if let array = array as? [Any] {
return Observable.just(array.flatMap { Mapper<T>().map(JSONObject: $0) })
}
return Observable.error(NSError(domain: "V2ex",
code: -1,
userInfo: ["Error" : "failed to map object array"]))
}
.observeOn(MainScheduler.instance)
}
}

mapObject将原始数据转换成单个模型,而mapObjectArray将原始数据转换成模型数组。使用Rx.flatMap是为了方便抛出错误,终止正常数据流的流动。

总的来说,这三者结合后写出来的代码给人一种畅快淋漓的感觉。不过在很多项目中,从后台获取的JSON也许不会那么规范,或者说层次分明,这样一来,需要处理的情况就复杂多了,对于上面的Rx扩展能否保持这个精简的体量还待观察。

文中提到的代码可以在这里这里找到。


参考

ObjectMapper

Moya

通过 Moya+RxSwift+Argo 完成网络请求