基于MVVM构建聊天App (三)网络请求封装

基于MVVM构建聊天App (三)网络请求封装

一月 05, 2021
小时光
本文主要处理2个问题:
  • 请求Loading扩展处理
  • 封装URLSession返回Observable序列

1、请求Loading扩展处理

关于Loading组件,我已经封装好,并发布在Github上,RPToastView,使用方法可参考README.md
此处只需对UIViewController做一个extension,用一个属性来控制Loading组件的显示和隐藏即可,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
extension Reactive where Base: UIViewController {
public var isAnimating: Binder<Bool> {
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包也同步变大,这并不是我所期望的
  • (4)、和RxSwift兼容问题,如RxSwift已经升级到6.0版本了,Moya却只支持5.*版本

所以我最终的选择是RxSwift+URLSession+SwiftyJSON

3、RxSwift的使用

关于网络请求,OC中常用的开源库是AFNetworking,在Swift中我们常用Alamofire。截止2020年12月AFNetworking的star数量是33.1K,Alamofire的star数量是35K。从这个数据来说,Swift虽然是一门新的语言,但更受开发者青睐。

网络请求最简单的方法个人觉得用 Alamofire通过Closures返回是否成功或失败:

1
func post(with body: [String : AnyObject], _ path: String, with closures: @escaping ((_ json: [String : AnyObject],_ failure : String?) -> Void))

如果我们在用户登录成功后需要再调一次接口查询该用户Socket服务器相关数据,那么请求的代码就会Closures里嵌套Closures

1
2
3
4
5
6
7
8
9
10
11
12
13
 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来写,就更符合逻辑了:

1
2
3
4
5
6
7
8
9
10
11
12
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方式,所需参数等,代码如下:

1
2
3
4
5
6
7
/// 请求服务器相关
public protocol Request {
var path: String {get}
var method: HTTPMethod {get}
var parameter: [String: AnyObject]? {get}
var host: String {get}
}

在发起一个请求时可能不需要任何参数,此处做一个extension处理将parameter作为可选参数即可:

1
2
3
4
5
extension Request {
var parameter: [String: AnyObject] {
return [:]
}
}

此处要分别对以上三种情况做出处理,首先来看看服务器给的接口文档,请求成功时服务器返回的数据结构:

1
2
3
4
5
6
7
8
9
10
11
{
"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"
}

请求成功,但出现异常时返回的数据结构:

1
2
3
4
{
"returnCode" : "601",
"returnMsg" : "登录失效",
}

新建一个SignInModel.Swift来作为模型

1
2
3
4
public struct SignInModel {
public let username,dept_id,access_token,token_type,user_id,scope,refresh_token,expires_in,license: String
}

将返回的SwiftyJSON对象转为Model对象

1
2
3
4
5
6
7
8
9
10
11
12
13
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来处理:

1
2
3
4
5
6
7
8
9
/// 请求服务器失败时 错误码
public enum RequestError: Error {
case unknownError
case connectionError
case timeoutError
case authorizationError(JSON)
case notFound
case serverError
}

4、发起请求并返回一个Observable对象

RxSwift对系统提供的URLSession也做了扩展,可以让开发者直接使用:

1
2
3
URLSession.shared.rx.response(request: urlRequest).subscribe(onNext: { (response, data) in

}).disposed(by: disposeBag)

首先定一个可以发送请求的协议, 无论请求成功还是失败都需要返回一个Observable队列,此处使用了一个**<T: Request>泛型,任何一个遵循AuthRemoteProtocol**的类型都可以实现网络请求。

1
2
3
public protocol AuthRemoteProtocol {
func post<T: Request>(_ r: T) -> Observable<JSON>
}

当发起一个请求时,我们需要对URLSession做一些请求配置,如设置header、body、url、timeout、请求方式等,才能顺利的完成一个请求。header、timeout这几个参数一般都固定的。而body、url这两个参数必须是一个遵循Request协议的对象。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public func post<T: Request>(_ r: T) -> Observable<JSON> {
// 设置请求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做出处理。最终的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/// 登录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<T: Request>(_ r: T) -> Observable<JSON>
}

public struct RPAuthRemoteAPI: AuthRemoteProtocol {
/// 协议方式,成功返回JSON -----> RxSwift
public func post<T: Request>(_ r: T) -> Observable<JSON> {
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做处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 显示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 中文网

泊学网