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

封面

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<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包也同步变大,这并不是我所期望的

所以我最终的选择是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队列,此处使用了一个<T: Request>泛型,任何一个遵循AuthRemoteProtocol的类型都可以实现网络请求。

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

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

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做出处理。最终的代码如下:

/// 登录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做处理:

// 显示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 中文网

泊学网

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