學習 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容