小时光
本文主要处理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:等待多个并发任务完成后处理结果
同时,使用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: { }
一般一个请求无非是三种情况:
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 中文网
泊学网