美文网首页iOS精学选辑SwiftRxSwift实战
学习 Swift Moya(二)- Moya + SwiftyJ

学习 Swift Moya(二)- Moya + SwiftyJ

作者: jkyeo | 来源:发表于2016-06-30 15:33 被阅读3539次

    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

    相关文章

      网友评论

      • 陌浅Ivan:能介绍下Moya如何上传图片吗?我总是上传失败。
        method是.post
        parameterEncoding是.default
        task是.upload(.multipart[ ])
        这样有问题吗?
        jkyeo:基于最新的 Moya 8.0.4 我是这样的:

        method: post
        parameterEncoding: URLEncoding.default
        task: Task.upload(.multipart([MultipartFormData.init(provider: .data(imageData), name: "file", fileName: "pic.jpeg", mimeType: "mage/jpeg")]))
      • 我把今生当成了来世:模型为什么不是继承Mappable哦?
      • Aaronn:moya 如何上传图片?
        Aaronn:@Aaronn 这样使用有什么好处?求赐教~
        Aaronn:@jkyeo 你的模型为啥要用结构体啊?~~..
        jkyeo:看官方文档
      • da27c260cc85:如果请求超时了,怎样能知道呢?
        jkyeo:会走 onError
      • derekibw:关于endpointClosure方面不是很明白,能加个微信咨询几个问题吗,微信号:derekibw
      • Ryan文濤:不错,点个赞。:smile:

      本文标题:学习 Swift Moya(二)- Moya + SwiftyJ

      本文链接:https://www.haomeiwen.com/subject/ysffjttx.html