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
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第三方庫,如果說出現系統版本升級,或者這些第三方庫的作者不再維護等問題,會給我們后期的開發和維護帶來很大的麻煩;
友情鏈接: