学习 Swift Moya(二)- Moya + SwiftyJSON + RxSwift

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

结合 Object Mapper 可以很方便的将 Moya.Response 转换成对象输出。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

然而前段开发遇到的接口往往是这样的:

{
    "resultCode":200,
    "resultMsg":"查询成功!",
    "data":{
        "city":"北京",
        "temperature":"8℃~20℃",
        "weather":"晴转霾"
    }
}

或者这样的:

{
    "resultCode":200,
    "resultMsg":"查询成功!",
    "data":[
        {
            "city":"北京",
            "temperature":"8℃~20℃",
            "weather":"晴转霾"
        },
        {
            "city":"南京",
            "temperature":"12℃~21℃",
            "weather":"晴"
        }
    ]
} 

也就是说,接口想要返回的业务数据外总是“包裹”了一层状态数据来标记这一次业务返回的成功、失败以及失败的原因。

那么,对 Moya.Response 做 map 处理后直接得到业务对象(也就是 "data" 下的数据,或为 object,或为 array) 岂不是更优雅?类似的问题在 Android 开放中有讨论过:

Retrofit + RxJava 业务状态重定向及分离

这篇文章就来讨论在 Moya + RxSwift 环境下如何实现这样的 mapper 数据分离

Moya + RxSwift + SwiftyJSON 业务状态重定向及分离

首先我们以聚合数据提供的电影票房查询接口为例:
对照 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,理论上是免费的,如果失效,请自行申请替换
                // 接口详情地址: https://www.juhe.cn/docs/api/id/44
                "key": "e8ec41002b1441dc9126d7bbf259b747"
            ]
        }
    }
    
    var sampleData: NSData {
        return "".dataUsingEncoding(NSUTF8StringEncoding)!
    }
}

这里补充一点,如果想要在每一次请求的 header 或者 params 中插入一些公关参数(如 platform, sys_ver 和 uid 等等),可以通过自定义 Endpoint Closure 方式实现。类似于 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 来实现更多的需求。具体可参见 Moya Docs

好了,言归正传。

分析接口返回的 json 数据:

{
  "resultcode": "200",
  "reason": "success",
  "result": [
    {
      "rid": "1",
      "name": "惊天魔盗团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,创建一个 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
    }
}

下面就是关键点了,怎样分离业务并且 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?) 是业务错误, 是前台与后台约定好如果 resultCode == “200” 表示业务成功,可以去 data 中取数据。其他数值表示失败,resultMsg 告知失败原因,比如“认真失败”、“key 过期”等等。

接下里,我们对上面的 json 进行处理,既然是使用 RxSwift,map 处理可以是扩展 Observable 方法实现,这样可以在 Rx chain 中调用 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
            }
            
        }
    }
}

最后在业务层,调用就很方便了:

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 下的业务数据不是一个 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
            }
        }
    }

好了,到这里任务算是完成了。

Demo

本文全部代码可运行示例已开源在 Github, 如果我讲的不够明白或者你有更好的解决方法,欢迎斧正、PR:
https://github.com/jkyeo/RxMoyaMapperDemo

Reference: Observable+Networking

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容