Github 基于MVVM构建聊天App (三)网络请求封装
本文主要处理2个问题:
- 请求Loading扩展处理
- 封装URLSession返回Observable序列
1、请求Loading扩展处理
关于Loading组件,我已经封装好,并发布在Github上,RPToastView,使用方法可参考README.md。
此处只需对UIViewController做一个extension,用一个属性来控制Loading组件的显示和隐藏即可,核心代码如下:
extension Reactive where Base: UIViewController {
public var isAnimating: Binder {
return Binder(self.base, binding: { (vc, active) in
if active == true {
// 显示Loading View
} else {
// 隐藏Loading View
}
})
}
}
此处给isAnimating传入true表示显示LoadingView,传入false表示隐藏LoadingView,
2、为什么不使用Moya
Github Moya
Moya是在常用的Alamofire的基础上又封装了一层,但是我在工程中并没有使用Moya,主要是基于以下3点考虑:
- (1)、Moya自身原因:Moya封装的很完美,这虽然为开发者带来了很大的方便,但是过多封装的必然会导致可扩展性下降
- (2)、内部原因:由于我公司的后台接口没有一个统一的标准,所以不同模块后台返回的数据结构不同,所以我不得不分开处理
- (3)、基于App包大小考虑:导入过多的第三方开源库必然会使App包也同步变大,这并不是我所期望的
所以我最终的选择是RxSwift+URLSession+SwiftyJSON。
3、RxSwift的使用
关于网络请求,OC中常用的开源库是AFNetworking,在Swift中我们常用Alamofire。截止2020年12月AFNetworking的star数量是33.1K,Alamofire的star数量是35K。从这个数据来说,Swift虽然是一门新的语言,但更受开发者青睐。
网络请求最简单的方法个人觉得用 Alamofire通过Closures返回是否成功或失败:
func post(with body: [String : AnyObject], _ path: String, with closures: @escaping ((_ json: [String : AnyObject],_ failure : String?) -> Void))
如果我们在用户登录成功后需要再调一次接口查询该用户Socket服务器相关数据,那么请求的代码就会Closures里嵌套Closures,
RPAuthRemoteAPI().signIn(with: ["username":"","password":""], signInAPI) { (siginInfo, errorMsg) in
if let errorMsg = errorMsg {
} else {
RPAuthRemoteAPI().socketInfo(with: ["username":""], userInfoAPI) { (userInfo, userInfoErrorMsg) in
if let userInfoErrorMsg = userInfoErrorMsg {
} else {
}
}
}
}
使用RxSwift可以将多个请求合并处理,参考RxSwift:等待多个并发任务完成后处理结果
- 1、更直观简洁的RxSwift
同时,使用RxSwift,返回一个Observable,还可以避免嵌套回调的问题。
上面的代码用RxSwift来写,就更符合逻辑了:
let _ = RPAuthRemoteAPI().signIn(with: ["username":"","password":""], signInAPI)
.flatMap({ (returnJson) in
return RPAuthRemoteAPI().userInfo(with: ["username":""], userInfoAPI)
}).subscribe { (json) in
print("用户信息-----------: \(json)")
} onError: { (error) in
} onCompleted: {
} onDisposed: {
}
- 2、处理服务器返回的数据
一般一个请求无非是三种情况:
- 请求成功时服务器返回的数据结构
- 请求服务器成功,但返回数据异常,如参数错误,加密处理异常,登录超时等
- 请求没有成功,根据返回的错误码做处理
创建一个协议来管理请求,此处需要知道请求的API,HTTP方式,所需参数等,代码如下:
/// 请求服务器相关
public protocol Request {
var path: String {get}
var method: HTTPMethod {get}
var parameter: [String: AnyObject]? {get}
var host: String {get}
}
在发起一个请求时可能不需要任何参数,此处做一个extension处理将parameter作为可选参数即可:
extension Request {
var parameter: [String: AnyObject] {
return [:]
}
}
此处要分别对以上三种情况做出处理,首先来看看服务器给的接口文档,请求成功时服务器返回的数据结构:
{
"access_token" : "b6298027-a985-441c-a36c-d0a362520896",
"user_id" : "1268805326995996673",
"dept_id" : 1,
"license" : "made by tsn",
"scope" : "server",
"token_type" : "bearer",
"username" : "198031",
"expires_in" : 19432,
"refresh_token" : "692a1b6e-051f-424d-bd2e-3a9ccec8d4f2"
}
请求成功,但出现异常时返回的数据结构:
{
"returnCode" : "601",
"returnMsg" : "登录失效",
}
新建一个SignInModel.Swift来作为模型
public struct SignInModel {
public let username,dept_id,access_token,token_type,user_id,scope,refresh_token,expires_in,license: String
}
将返回的SwiftyJSON对象转为Model对象
extension SignInModel {
public init?(json: JSON) {
username = json["username"].stringValue
dept_id = json["dept_id"].stringValue
access_token = json["access_token"].stringValue
token_type = json["token_type"].stringValue
user_id = json["user_id"].stringValue
scope = json["scope"].stringValue
refresh_token = json["refresh_token"].stringValue
expires_in = json["expires_in"].stringValue
license = json["license"].stringValue
}
}
当请求成功后,将服务器获取的Data数据转成SwiftyJSON实例,然后在ViewModel中转成SignInModel。
对于请求成功时,但返回数据异常时,可根据后台返回的code码和message信息,给用户一个友好提示。
对于请求服务器失败时情况,可以定义一个enum来处理:
/// 请求服务器失败时 错误码
public enum RequestError: Error {
case unknownError
case connectionError
case timeoutError
case authorizationError(JSON)
case notFound
case serverError
}
4、发起请求并返回一个Observable对象
RxSwift
对系统提供的URLSession
也做了扩展,可以让开发者直接使用:
URLSession.shared.rx.response(request: urlRequest).subscribe(onNext: { (response, data) in
}).disposed(by: disposeBag)
首先定一个可以发送请求的协议, 无论请求成功还是失败都需要返回一个Observable队列,此处使用了一个
public protocol AuthRemoteProtocol {
func post(_ r: T) -> Observable
}
当发起一个请求时,我们需要对URLSession做一些请求配置,如设置header、body、url、timeout、请求方式等,才能顺利的完成一个请求。header、timeout这几个参数一般都固定的。而body、url这两个参数必须是一个遵循Request协议的对象。核心代码如下:
public func post(_ r: T) -> Observable {
// 设置请求API
guard let path = URL(string: r.host.appending(r.path)) else {
return .error(RequestError.unknownError)
}
var headers: [String : String]?
// 设置超时时间
var urlRequest = URLRequest(url: path, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30)
// 设置header
urlRequest.allHTTPHeaderFields = headers
// 设置请求方式
urlRequest.httpMethod = r.method.rawValue
return Observable.create { (observer) -> Disposable in
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
// 根据服务器返回的code处理并传递给ViewModel
}.resume()
return Disposables.create { }
}
}
一般跟服务器约定,当服务器返回的code为200时我们认为服务器请求成功并正常返回数据,当返回其他code
时根据返回的code做出处理。最终的代码如下:
/// 登录Request
struct SigninRequest: Request {
typealias Response = SigninRequest
var parameter: [String : AnyObject]?
var path: String
var method: HTTPMethod = .post
var host: String {
return __serverTestURL
}
}
public enum RequestError: Error {
case unknownError
case connectionError
case timeoutError
case authorizationError(JSON)
case notFound
case serverError
}
public protocol AuthRemoteProtocol {
/// 协议方式,成功返回JSON -----> RxSwift
func requestData(_ r: T) -> Observable
}
public struct RPAuthRemoteAPI: AuthRemoteProtocol {
/// 协议方式,成功返回JSON -----> RxSwift
public func post(_ r: T) -> Observable {
let path = URL(string: r.host.appending(r.path))!
var urlRequest = URLRequest(url: path, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30)
urlRequest.allHTTPHeaderFields = ["Content-Type" : "application/x-www-form-urlencoded; application/json; charset=utf-8;"]
urlRequest.httpMethod = r.method.rawValue
if let parameter = r.parameter {
// --> Data
let parameterData = parameter.reduce("") { (result, param) -> String in
return result + "&\(param.key)=\(param.value as! String)"
}.data(using: .utf8)
urlRequest.httpBody = parameterData
}
return Observable.create { (observer) -> Disposable in
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print(error)
observer.onError(RequestError.connectionError)
} else if let data = data ,let responseCode = response as? HTTPURLResponse {
do {
let json = try JSON(data: data)
switch responseCode.statusCode {
case 200:
print("json-------------\(json)")
observer.onNext(json)
observer.onCompleted()
break
case 201...299:
observer.onError(RequestError.authorizationError(json))
break
case 400...499:
observer.onError(RequestError.authorizationError(json))
break
case 500...599:
observer.onError(RequestError.serverError)
break
case 600...699:
observer.onError(RequestError.authorizationError(json))
break
default:
observer.onError(RequestError.unknownError)
break
}
}
catch let parseJSONError {
observer.onError(parseJSONError)
print("error on parsing request to JSON : \(parseJSONError)")
}
}
}.resume()
return Disposables.create { }
}
}
在ViewModel中调用,并根据服务器返回的code做处理:
// 显示LoadingView
self.loading.onNext(true)
RPAuthRemoteAPI().post(SigninRequest(parameter: [:], path: path))
.subscribe(onNext: { returnJson in
// JSON对象转成Model,同时本地缓存Token
self.loading.onNext(true)
}, onError: { errorJson in
// 失败
self.loading.onNext(true)
}, onCompleted: {
// 调用完成时
}).disposed(by: disposeBag)
5、存在问题
虽然以上的方法基于POP的实现,利于代码的扩展和维护。但是我觉得也存在问题:
- 过分依赖RxSwift、SwiftyJSON第三方库,如果说出现系统版本升级,或者这些第三方库的作者不再维护等问题,会给我们后期的开发和维护带来很大的麻烦;
友情链接:
面向协议编程与 Cocoa 的邂逅
Sample Music list app
Github RxSwift
RxSwift 中文网
泊学网