Moya + RxSwift
Moya + RxSwift 最簡單的使用方法是這樣的:
provider = RxMoyaProvider<ApiService>()
provider.request(ApiService.Function("param")).subscribe { (event) -> Void in
switch event {
case .Next(let response):
// do something like refresh ui
case .Error(let error):
print(error)
default:
break
}
}
Object Mapper
結(jié)合 Object Mapper 可以很方便的將 Moya.Response 轉(zhuǎn)換成對象輸出枫攀。Moya 官方也給出了幾個典型的 ObjectMapper Extension :
- Moya-ObjectMapper - ObjectMapper bindings for Moya for easier JSON serialization
- Moya-SwiftyJSONMapper - SwiftyJSON bindings for Moya for easier JSON serialization
- Moya-Argo - Argo bindings for Moya for easier JSON serialization
- Moya-ModelMapper - ModelMapper bindings for Moya for easier JSON serialization
- Moya-Gloss - Gloss bindings for Moya for easier JSON serialization
- Moya-JASON - JASON bindings for Moya for easier JSON serialization
然而前段開發(fā)遇到的接口往往是這樣的:
{
"resultCode":200,
"resultMsg":"查詢成功!",
"data":{
"city":"北京",
"temperature":"8℃~20℃",
"weather":"晴轉(zhuǎn)霾"
}
}
或者這樣的:
{
"resultCode":200,
"resultMsg":"查詢成功!",
"data":[
{
"city":"北京",
"temperature":"8℃~20℃",
"weather":"晴轉(zhuǎn)霾"
},
{
"city":"南京",
"temperature":"12℃~21℃",
"weather":"晴"
}
]
}
也就是說狐榔,接口想要返回的業(yè)務(wù)數(shù)據(jù)外總是“包裹”了一層狀態(tài)數(shù)據(jù)來標(biāo)記這一次業(yè)務(wù)返回的成功呢堰、失敗以及失敗的原因狂巢。
那么,對 Moya.Response 做 map 處理后直接得到業(yè)務(wù)對象(也就是 "data" 下的數(shù)據(jù)夯尽,或為 object脉顿,或為 array) 豈不是更優(yōu)雅糕档?類似的問題在 Android 開放中有討論過:
這篇文章就來討論在 Moya + RxSwift 環(huán)境下如何實現(xiàn)這樣的 mapper 數(shù)據(jù)分離
Moya + RxSwift + SwiftyJSON 業(yè)務(wù)狀態(tài)重定向及分離
首先我們以聚合數(shù)據(jù)提供的電影票房查詢接口為例:
對照 Moya Docs,很容易的建立以下 ApiService:
let apiProvider = RxMoyaProvider<ApiService>()
enum ApiService {
case GetRank(area: String?)
}
extension ApiService: TargetType {
var baseURL: NSURL {return NSURL(string: "http://v.juhe.cn")!}
var path: String {
switch self {
case .GetRank(_):
return "/boxoffice/rank"
}
}
var method: Moya.Method {
return .GET
}
var parameters: [String: AnyObject]? {
switch self {
case .GetRank(let area):
return [
"area": nil == area ? "" : area!,
// 這里是我的測試 key痊硕,理論上是免費的赊级,如果失效,請自行申請?zhí)鎿Q
// 接口詳情地址: https://www.juhe.cn/docs/api/id/44
"key": "e8ec41002b1441dc9126d7bbf259b747"
]
}
}
var sampleData: NSData {
return "".dataUsingEncoding(NSUTF8StringEncoding)!
}
}
這里補(bǔ)充一點岔绸,如果想要在每一次請求的 header 或者 params 中插入一些公關(guān)參數(shù)(如 platform, sys_ver 和 uid 等等)理逊,可以通過自定義 Endpoint Closure 方式實現(xiàn)。類似于 Android Okhttp 中的 Network Intercepor:
let headerFields: Dictionary<String, String> = [
"platform": "iOS",
"sys_ver": String(UIDevice.version())
]
let appendedParams: Dictionary<String, AnyObject> = [
"uid": "123456"
]
let endpointClosure = { (target: ApiService) -> Endpoint<ApiService> in
let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
return Endpoint(URL: url, sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters)
.endpointByAddingParameters(appendedParams)
.endpointByAddingHTTPHeaderFields(headerFields)
然后 apiProvider 的初始化就是這樣的:
let apiProvider = RxMoyaProvider<ApiService>(endpointClosure: endpointClosure)
更多的亭螟,我們還可以自定義 requestClosure, stubClosure, manager 和 plugins 來實現(xiàn)更多的需求挡鞍。具體可參見 Moya Docs
好了,言歸正傳预烙。
分析接口返回的 json 數(shù)據(jù):
{
"resultcode": "200",
"reason": "success",
"result": [
{
"rid": "1",
"name": "驚天魔盜團(tuán)2",
"wk": "2016.6.20 - 2016.6.26(單位:萬元)",
"wboxoffice": "28690",
"tboxoffice": "28690"
},
{
"rid": "2",
"name": "獨立日:卷土重來",
"wk": "2016.6.20 - 2016.6.26(單位:萬元)",
"wboxoffice": "23924",
"tboxoffice": "23924"
}
],
"error_code": 0
}
我們選用 SwiftyJSON 來 map json,創(chuàng)建一個 Protocol:
public protocol Mapable {
init?(jsonData:JSON)
}
建立 BoxofficeModel 模型:
struct BoxofficeModel: Mapable {
let rid: String?
let name: String?
let wk: String?
let wboxoffice: String?
let tboxoffice: String?
init?(jsonData: JSON) {
self.rid = jsonData["rid"].string
self.name = jsonData["name"].string
self.wk = jsonData["wk"].string
self.wboxoffice = jsonData["wboxoffice"].string
self.tboxoffice = jsonData["tboxoffice"].string
}
}
下面就是關(guān)鍵點了道媚,怎樣分離業(yè)務(wù)并且 map to objectArray扁掸?Show me the code:
首先定義集中錯誤:
enum ORMError : ErrorType {
case ORMNoRepresentor
case ORMNotSuccessfulHTTP
case ORMNoData
case ORMCouldNotMakeObjectError
case ORMBizError(resultCode: Int?, resultMsg: String?)
}
其中 ORMBizError(resultCode: Int?, resultMsg: String?) 是業(yè)務(wù)錯誤, 是前臺與后臺約定好如果 resultCode == “200” 表示業(yè)務(wù)成功最域,可以去 data 中取數(shù)據(jù)谴分。其他數(shù)值表示失敗,resultMsg 告知失敗原因镀脂,比如“認(rèn)真失敗”牺蹄、“key 過期”等等。
接下里薄翅,我們對上面的 json 進(jìn)行處理沙兰,既然是使用 RxSwift,map 處理可以是擴(kuò)展 Observable 方法實現(xiàn)翘魄,這樣可以在 Rx chain 中調(diào)用 map 方法:
enum ORMError : ErrorType {
case ORMNoRepresentor
case ORMNotSuccessfulHTTP
case ORMNoData
case ORMCouldNotMakeObjectError
case ORMBizError(resultCode: String?, resultMsg: String?)
}
enum BizStatus: String {
case BizSuccess = "200"
case BizError
}
public protocol Mapable {
init?(jsonData:JSON)
}
let RESULT_CODE = "resultcode"
let RESULT_MSG = "reason"
let RESULT_DATA = "result"
extension Observable {
private func resultFromJSON<T: Mapable>(jsonData:JSON, classType: T.Type) -> T? {
return T(jsonData: jsonData)
}
func mapResponseToObjArray<T: Mapable>(type: T.Type) -> Observable<[T]> {
return map { response in
// get Moya.Response
guard let response = response as? Moya.Response else {
throw ORMError.ORMNoRepresentor
}
// check http status
guard ((200...209) ~= response.statusCode) else {
throw ORMError.ORMNotSuccessfulHTTP
}
// unwrap biz json shell
let json = JSON.init(data: response.data)
// check biz status
if let code = json[RESULT_CODE].string {
if code == BizStatus.BizSuccess.rawValue {
// bizSuccess -> wrap and return biz obj array
var objects = [T]()
let objectsArrays = json[RESULT_DATA].array
if let array = objectsArrays {
for object in array {
if let obj = self.resultFromJSON(object, classType:type) {
objects.append(obj)
}
}
return objects
} else {
throw ORMError.ORMNoData
}
} else {
throw ORMError.ORMBizError(resultCode: json[RESULT_CODE].string, resultMsg: json[RESULT_MSG].string)
}
} else {
throw ORMError.ORMCouldNotMakeObjectError
}
}
}
}
最后在業(yè)務(wù)層鼎天,調(diào)用就很方便了:
let disposeBag = DisposeBag()
apiProvider.request(ApiService.GetRank(area: "CN"))
.mapResponseToObjArray(BoxofficeModel)
.subscribe(
onNext: { items in
// do somethong like refresh ui
},
onError: { error in
print(error)
}
)
.addDisposableTo(disposeBag)
如果 json data 下的業(yè)務(wù)數(shù)據(jù)不是一個 array 而只是一個 object 怎么辦呢?其實方法大同小異;
func mapResponseToObj<T: Mapable>(type: T.Type) -> Observable<T?> {
return map { representor in
// get Moya.Response
guard let response = representor as? Moya.Response else {
throw ORMError.ORMNoRepresentor
}
// check http status
guard ((200...209) ~= response.statusCode) else {
throw ORMError.ORMNotSuccessfulHTTP
}
// unwrap biz json shell
let json = JSON.init(data: response.data)
// check biz status
if let code = json[RESULT_CODE].string {
if code == BizStatus.BizSuccess.rawValue {
// bizSuccess -> return biz obj
return self.resultFromJSON(json[RESULT_DATA], classType:type)
} else {
// bizError -> throw biz error
throw ORMError.ORMBizError(resultCode: json[RESULT_CODE].string, resultMsg: json[RESULT_MSG].string)
}
} else {
throw ORMError.ORMCouldNotMakeObjectError
}
}
}
好了暑竟,到這里任務(wù)算是完成了斋射。
Demo
本文全部代碼可運行示例已開源在 Github, 如果我講的不夠明白或者你有更好的解決方法,歡迎斧正但荤、PR:
https://github.com/jkyeo/RxMoyaMapperDemo
Reference: Observable+Networking