學(xué)習(xí) 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

結(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 開放中有討論過:

Retrofit + RxJava 業(yè)務(wù)狀態(tài)重定向及分離

這篇文章就來討論在 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末罗岖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子腹躁,更是在濱河造成了極大的恐慌桑包,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件潜慎,死亡現(xiàn)場離奇詭異捡多,居然都是意外死亡蓖康,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門垒手,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒜焊,“玉大人,你說我怎么就攤上這事科贬∮景穑” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵榜掌,是天一觀的道長优妙。 經(jīng)常有香客問我,道長憎账,這世上最難降的妖魔是什么套硼? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮胞皱,結(jié)果婚禮上邪意,老公的妹妹穿的比我還像新娘。我一直安慰自己反砌,他們只是感情好雾鬼,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宴树,像睡著了一般策菜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酒贬,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天又憨,我揣著相機(jī)與錄音,去河邊找鬼同衣。 笑死竟块,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的耐齐。 我是一名探鬼主播浪秘,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼埠况!你這毒婦竟也來了耸携?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤辕翰,失蹤者是張志新(化名)和其女友劉穎夺衍,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喜命,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡沟沙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年河劝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矛紫。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡赎瞎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出颊咬,到底是詐尸還是另有隱情务甥,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布喳篇,位于F島的核電站敞临,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏麸澜。R本人自食惡果不足惜挺尿,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望炊邦。 院中可真熱鬧票髓,春花似錦、人聲如沸铣耘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜗细。三九已至,卻和暖如春搓译,著一層夾襖步出監(jiān)牢的瞬間瘦陈,已是汗流浹背器躏。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留吊骤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓静尼,卻偏偏與公主長得像白粉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鼠渺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容