基于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 中文網

泊學網

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。