Moya + RxSwift + ObjectMapper / HandyJson 创造简洁的网络请求代码

2017.07.05更新:

  • 移除Alamofire 不支持iOS8解决方案,Alamofire 在我当时项目立项的时候是不支持iOS8的,所以引入了不支持iOS8解决方案,但是之后不久就不固执的要求 iOS9+ 了,所以这篇博客里的解决方案部分就没有什么意义了,徒增篇幅,也就移除了。
  • 增加了Demo ,点击传送至Demo
  • 增加了HanyJSON

Alamofire

在Swift中我们发送网络请求一般都是使用一个第三方库 Alamofire ,设置好URL和parameter然后发送网络请求,就像下面这样:

// Alamofire 4
let parameters: Parameters = ["foo": "bar"]

Alamofire.request(urlString, method: .get, parameters: parameters, encoding: JSONEncoding.default)
    .downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
        print("Progress: \(progress.fractionCompleted)")
    }
    .validate { request, response, data in
        // Custom evaluation closure now includes data (allows you to parse data to dig out error messages if necessary)
        return .success
    }
    .responseJSON { response in
        debugPrint(response)
    }

当然这是Alamofire官方给出的最简单使用例子,你要真的在项目里每个网络请求都这么写。。那真的是太耿直了,来一次网络库版本更新,你就知道什么是痛苦了,项目里遍地都是要改的代码。

给Alamofire加个Router

我们在项目开发中,很少像上边那样直接拿来用,这篇不是Alamofire的教程,感兴趣的可以到 这里 (这里是国人翻译版)看看是如何从零开始一步步把Alamofire封装成一个更加好用的框架的。

如果只想用 Alamofire不想引入 Moya我的demo里的Router.swift文件 也提供好了类似的封装,可以直接复制,稍微修改一下就可以拿来用。点击跳转

使用封装好的Alamofire只需要这样:

// get请求  根据不同需求,可以在router里 自定义网络请求需要的字段, 业务代码只需要关心传入uid 和 info,然后处理请求到的response
Alamofire.request(Router.getInfo(uid: "1", token: "abc")).responseJSON { response in
    // 在这里处理 请求到的数据
    debugPrint(response)
}

// post请求  根据不同需求,可以在router里 自定义网络请求需要的字段, 业务代码只需要关心传入uid 和 info,然后处理请求到的response
Alamofire.request(Router.postInfo(uid: "1", info: "110")).responseJSON { response in
    // 在这里处理 请求到的数据
    debugPrint(response)
}

比上边的直接用看起来简洁多了。

Moya

其实Moya已经把我们上边做的都写好了,使用moya进行网络请求是这样的:

provider.request(.zen) { result in
    ...
    ...
}

也很简洁。同样在我的demo里的Moya.swift文件 也提供好了Moya在项目里的写法,可以直接复制,稍微修改一下就可以拿来用。点击跳转,在项目里使用见ViewController.swift,是这样的:

let provider = MoyaProvider<MyAPI>()
provider.request(.login(username: "username", password: "password")) { result in
    debugPrint(result)
}

Moya + RxSwift

接下来我们看看 Moya配合上RxSwift能有什么不同的魔法。因为我们要用的是Moya提供的Rx扩展,所以Podfile里要这样写:

pod 'Moya/RxSwift'

发起网络请求使用起来和单独使用Moya差不多:

let rxProvider = RxMoyaProvider<MyAPI>()
rxProvider(.login(username: "username", password: "password")) { result in
    debugPrint(result)
}

但是,配合起来Rx,Moya提供了更多的方便用法。

  • 比如我们刚才写的代码,其实对请求到的错误信息都没有处理。我们现在可以这样写:
rxProvider.request(.login(username: "username", password: "password"))
    .filterSuccessfulStatusCodes()
    .subscribe(onNext: {
        debugPrint($0)
    })
    .addDisposableTo(disposeBag)
  • 想要过滤出特定的code?比如说在用户密码更改时和后台约定的code:
rxProvider.request(.login(username: "username", password: "password"))
    .filter(statusCode: 0)
    .subscribe(onNext: {
        debugPrint($0)
        //在这里添加处理返回特定code的逻辑
    })
    .addDisposableTo(disposeBag)
  • 还有提供过滤 一个范围的code:
.filter(statusCodes: 200...300)
  • 一句话把请求到的数据转成JSON
rxProvider.request(.login(username: "username", password: "password"))
    .mapJSON()
    .subscribe(onNext: { json in
        debugPrint(json) //已经帮你转成了 JSON,在这里只需要拿到JSON进行接下来的逻辑就可以了
    })
    .addDisposableTo(disposeBag)
  • 还可以mapString
.mapString(atKeyPath: "")

Moya + RxSwift + ObjectMapper

在上一部分,我们已经可以将网络请求回来的数据一键转为JSON了,项目中肯定是经常会遇到将JSON转为对象的操作,ObjectMapper 就是Swift中常用的JSON转对象框架。

将JSON转为对象:

let object = Mapper<T>().map(JSON: data)!

具体详细用法可以看看项目说明,这里默认已经是有过Swift项目经验,使用过ObjectMapper,就不讲解ObjectMapper的用法了,因为如果写出来,这篇文章就太长了。如果你用的是HandyJson,请看下一节的介绍。

接下来,我们把json解析为对象的过程也简化一下。

我们请求回来的数据可能是这样的:

{
    "code" : 200
   "message" : "success"
    "data" : {
        "user" : {
        "nickname" : "name"
        "age" : 18
        }
    }
}

也可能是这样的:

{   
    "code" : 200
    "message" : "success"
    "data" : [

        "user" : {
            "nickname" : "name1"
            "age" : 18
        },

        "user" : {
            "nickname" : "name2"
            "age" : 19
        },

        "user" : {
            "nickname" : "name3"
            "age" : 13
        }
    ]

}

这就要求我们能把JSON解析为一个单独的User对象,也要能解析成一个User对象数组

ObjectMapper + RxSwift

Podfile中添加:

pod 'ObjectMapper', '~> 2.2'

然后记得pod install

接下来,创建RxMoyaMapper.swift文件
因为网络请求和解析的过程中,会有各种错误出现,首先要在文件中定义错误类型

enum DCUError : Swift.Error {
    // 解析失败
    case ParseJSONError
    // 网络请求发生错误
    case RequestFailed
    // 接收到的返回没有data
    case NoResponse
    //服务器返回了一个错误代码
    case UnexpectedResult(resultCode: String?, resultMsg: String?)
}

然后定义一个RequestStatus的枚举,用来表示服务器返回给我们的code是不是约定的成功code,比方约定返回200为认证成功,401表示用户登录信息失效,等等,那么我们是需要在返回认证不成功code时,进行一些逻辑处理的(比方弹出登录框,让用户重新登录)。

enum RequestStatus: Int {
    case requestSuccess = 200
    case requestError
}

定义 code ,message ,data字段,方便后边解析时候使用(请根据后台返回内容不同做相应修改,这里是拿我们后台的返回规则举例):

let RESULT_CODE = "code"
let RESULT_MSG = "message"
let RESULT_DATA = "data"

然后我们给RxSwift 的Observable添加extension,两个方法分别对应解析为一个单独的User对象,和解析成一个User对象数组:

extension Observable {
    func mapResponseToObject<T: BaseMappable>(type: T.Type) -> Observable<T> {
        return map { response in
            
            // get Moya.Response
            guard let response = response as? Moya.Response else {
                throw DCUError.NoResponse
            }
            
            // check http status
            guard ((200...209) ~= response.statusCode) else {
                throw DCUError.RequestFailed
            }
            
            guard let json = try? JSONSerialization.jsonObject(with: response.data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as! [String: Any]  else {
                throw DCUError.NoResponse
            }
            
            
            if let code = json[RESULT_CODE] as? Int {
                if code == RequestStatus.requestSuccess.rawValue {
                    let data =  json[RESULT_DATA]
                    if let data = data as? [String: Any] {
                        let object = Mapper<T>().map(JSON: data)!
                        return object
                    }else {
                        throw DCUError.ParseJSONError
                    }
                } else {
                    throw DCUError.UnexpectedResult(resultCode: json[RESULT_CODE] as? Int , resultMsg: json[RESULT_MSG] as? String)
                }
            } else {
                throw DCUError.ParseJSONError
            }
            
        }
    }
    
    func mapResponseToObjectArray<T: BaseMappable>(type: T.Type) -> Observable<[T]> {
        return map { response in
            
            // get Moya.Response
            guard let response = response as? Moya.Response else {
                throw DCUError.NoResponse
            }
            
            // check http status
            guard ((200...209) ~= response.statusCode) else {
                throw DCUError.RequestFailed
            }

            guard let json = try? JSONSerialization.jsonObject(with: response.data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as! [String: Any]  else {
                throw DCUError.NoResponse
            }
            
            
            if let code = json[RESULT_CODE] as? Int {
                if code == RequestStatus.requestSuccess.rawValue {
                    var objects = [T]()
                    guard let objectsArrays = json[RESULT_DATA] as? [Any] else {
                        throw DCUError.ParseJSONError
                    }
                    for object in objectsArrays {
                        if let data = object as? [String: Any] {
                            let object = Mapper<T>().map(JSON: data)!
                            objects.append(object)
                        }
                    }
                    return objects

                } else {
                    throw DCUError.UnexpectedResult(resultCode: json[RESULT_CODE] as? Int , resultMsg: json[RESULT_MSG] as? String)

                }
            } else {
                throw DCUError.ParseJSONError
            }
        }
    }
}

同样在我的demo里的RxMoyaMapper.swift文件, 也提供好了完整的实现,可以现在项目看一下,稍微修改一下就可以拿来在项目里使用。点击跳转

在项目里写起来是这样的:

//解析成User对象
let rxProvider = RxMoyaProvider<MyAPI>()
rxProvider.request(.login(username: "username", password: "password"))
    .mapResponseToObject(type: User.self)
    .subscribe(onNext: { user in
        debugPrint(user)
    })
    .addDisposableTo(disposeBag)

//解析成User对象数组
rxProvider.request(.login(username: "username", password: "password"))
    .mapResponseToObjectArray(type: User.self)
    .subscribe(onNext: { users in
        debugPrint(users)
    })
    .addDisposableTo(disposeBag)

Moya + RxSwift + ObjectMapper

首先在Podfile中添加:

pod 'HandyJSON', '~> 1.7.2'

然后

pod install

我们可以直接来之前定义的DCUError来用,其他的实现也都很类似,只需要把解析的部分换成handyJSON就行了:

let object = JSONDeserializer<T>.deserializeFrom(json: jsonString)
let objArray = JSONDeserializer<T>.deserializeModelArrayFrom(array: objectsArrays)

使用起来也是一模一样的:

func handyJSON() {
    //解析成People对象
    let rxProvider = RxMoyaProvider<MyAPI>()
    rxProvider.request(.login(username: "username", password: "password"))
        .mapResponseToObject(type: People.self)
        .subscribe(onNext: { _ in
            
        })
        .addDisposableTo(disposeBag)
    
    //解析成People对象数组
    rxProvider.request(.login(username: "username", password: "password"))
        .mapResponseToObjectArray(type: People.self)
        .subscribe(onNext: { users in
            debugPrint(users)
        })
        .addDisposableTo(disposeBag)
}

同样,handyJSON的代码我也放在我的demo里的RxHandyJSON.swift文件了, 提供好了完整的实现,可以现在项目看一下,稍微修改一下就可以拿来在项目里使用。点击跳转

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

推荐阅读更多精彩内容