Swift 掌控Moya的網路請求、資料解析與快取
-
Moya
在Swift開發中起著重要的網路互動作用,但是還有不如之處,比如網路不可用時,返回的Response
為nil
,這時還得去解析相應的Error
-
Codable
可以幫助我們快速的解析資料,但是一旦宣告的屬性型別與json中的不一致,將無法正常解析; 而且對於模型中自定義屬性名的處理也十分繁瑣
解決的方案有很多,不過我比較習慣使用 MoyaMapper
,不僅可以解決上述問題,還提供了多種 模型轉換
、 資料互轉
、 多種資料型別任意儲存
的便捷方法。掌控Moya的網路請求、資料解析與快取簡直易如反掌。
MoyaMapper
是基於Moya和SwiftyJSON封裝的工具,以Moya的plugin的方式來實現間接解析,支援RxSwift
GitHub: ofollow,noindex">MoyaMapper
:book: 詳細的使用請檢視手冊MoyaMapper.github.io
特點
- 支援
json
轉Model
自動對映 與 自定義對映 - 無視
json
中值的型別,Model
中屬性宣告的是什麼型別,它就是什麼型別 - 支援
json字串
轉Model
- 外掛方式,全方位保障
Moya.Response
,拒絕各種網路問題導致Response
為nil
,將各式各樣的原因導致的資料載入失敗進行統一處理,開發者只需要關注Response
- 可選 - 支援資料隨意快取(
JSON
、Number
、String
、Bool
、Moya.Response
) - 可選 - 支援網路請求快取
資料解析
一、外掛注入

1、定義適用於專案介面的 ModelableParameterType
// statusCodeKey、tipStrKey、 modelKey 可以任意指定級別的路徑,如: "error>used" struct NetParameter : ModelableParameterType { var successValue = "000" var statusCodeKey = "retStatus" var tipStrKey = "retMsg" var modelKey = "retBody" } 複製程式碼
2、在 MoyaProvider
中使用 MoyaMapperPlugin
外掛,並指定 ModelableParameterType
let lxfNetTool = MoyaProvider<LXFNetworkTool>(plugins: [MoyaMapperPlugin(NetParameter())]) 複製程式碼
:exclamation: 使用 MoyaMapperPlugin
外掛是整個 MoyaMapper
的核心所在!
二、Model宣告
Model
需遵守 Modelable
協議
MoyaMapper Model
1、一般情況下如下寫法即可
struct CompanyModel: Modelable { var name : String = "" var catchPhrase : String = "" init() { } } 複製程式碼
2、如果自定義對映,則可以實現方法 mutating func mapping(_ json: JSON)
struct CompanyModel: Modelable { var name : String = "" var catchPhrase : String = "" init() { } mutating func mapping(_ json: JSON) { self.name = json["nickname"].stringValue } } 複製程式碼
3、支援模型巢狀
struct UserModel: Modelable { var id : String = "" var name : String = "" var company : CompanyModel = CompanyModel() init() { } } 複製程式碼
三、Response 解析
1、以下示例皆使用了 MoyaMapperPlugin
,所以不需要指定 解析路徑
2、如果沒有使用 MoyaMapperPlugin
則需要指定 解析路徑
,否則無法正常解析
ps: 解析路徑
可以使用 a>b
這種形式來解決多級路徑的問題
如果介面請求後 json
的資料結構與下圖類似,則使用 MoyaMapper
是最合適不過了

// Normal let model = response.mapObject(MMModel.self) print("name -- \(model.name)") print("github -- \(model.github)") // 列印json print(response.fetchJSONString()) // Rx rxRequest.mapObject(MMModel.self) .subscribe(onSuccess: { (model) in print("name -- \(model.name)") print("github -- \(model.github)") }).disposed(by: disposeBag) 複製程式碼

// Normal let models = response.mapArray(MMModel.self) let name = models[0].name print("count -- \(models.count)") print("name -- \(name)") // 列印 json 模型陣列中第一個的name print(response.fetchString(keys: [0, "name"])) // Rx rxRequest.mapArray(MMModel.self) .subscribe(onSuccess: { models in let name = models[0].name print("count -- \(models.count)") print("name -- \(name)") }).disposed(by: disposeBag) 複製程式碼

// Normal let (isSuccess, tipStr) = response.mapResult() print("isSuccess -- \(isSuccess)") print("tipStr -- \(tipStr)") // Rx rxRequest.mapResult() .subscribe(onSuccess: { (isSuccess, tipStr) in print("isSuccess -- \(isSuccess)") // 是否為 "000" print("retMsg -- \(retMsg)") // "缺少必要引數" }).disposed(by: disposeBag) 複製程式碼
統一處理網路請求結果
在APP的實際使用過程中,會遇到各種各樣的網路請求結果,如:伺服器掛了、手機無網路,此時 Moya
返回的 Response
為 nil,這樣我們就不得不去判斷 Error
。但是使用 MoyaMapperPlugin
就可以讓我們只關注 Response
// MoyaMapperPlugin 的初始化方法 public init<T: ModelableParameterType>( _ type: T, transformError: Bool = true ) type : ModelableParameterType用於定義欄位路徑,做為全域性解析資料的依據 transformError : Bool是否當網路請求失敗時,自動轉換請求結果,預設為 true 複製程式碼
- 當請求失敗的時候,此時的
result.response
為nil
,根據transformError
是否為true
判斷是否建立一個自定義的response
並返回出去。
➡ 本來可以請求到的資料內容

➡ 現在關閉網路,再請求資料
-
正常情況下,即不做任何不處理的時候,
Response
為nil
-
經過
MoyaMapperPlugin
處理的後可得到轉換後的Response
,如圖

這裡將請求失敗進行了統一處理,無論是伺服器還是自身網路的問題, retStatus
都為 MMStatusCode.loadFail,但是 errorDescription 會保持原來的樣子並賦值給 retMsg
。
-
retStatus
值會從列舉MMStatusCode
中取loadFail.rawValue
,即700
- 取 型別為
ModelableParameterType
的type
中statusCodeKey
所指定的值 為鍵名,retMsg
也同理
ps: 這個時候可以通過判斷 retStatus
或 response.statusCode
是否與 MMStatusCode.loadFail.rawValue
相同來判斷是否顯示載入失敗的空白頁佔位圖
enum MMStatusCode: Int { case cache = 230 case loadFail = 700 } 複製程式碼
列舉 MMStatusCode
中除了 loadFail
,還有 cache
,我們已經知道 loadFail
在資料載入失敗的時候會出現,那 cache
是在什麼時候出來呢?不急,看下一節就知道了。
資料快取
一、基本使用
// 快取 @discardableResult MMCache.shared.cache`XXX`(value : XXX, key: String, cacheContainer: MMCache.CacheContainer = .RAM)-> Bool // 取捨 MMCache.shared.fetch`XXX`Cache(key: String, cacheContainer: MMCache.CacheContainer = .RAM) 複製程式碼
快取成功會返回一個 Bool
值,這裡可不接收
XXX 所支援型別 | |
---|---|
Bool | - |
Float | - |
Double | - |
String | - |
JSON | - |
Modelable | [Modelable] |
Moya.Response | - |
Int | UInt |
Int8 | UInt8 |
Int16 | UInt16 |
Int32 | UInt32 |
Int64 | UInt64 |
其中,除了 Moya.Response
之外,其它型別皆是通過 JSON
來實現快取
所以,如果你想清除這些型別的快取,只需要呼叫如下方法即可
@discardableResult func removeJSONCache(_ key: String, cacheContainer: MMCache.CacheContainer = .RAM) -> Bool @discardableResult func removeAllJSONCache(cacheContainer: MMCache.CacheContainer = .RAM) -> Bool 複製程式碼
清除 Moya.Response
則使用如下兩個方法
@discardableResult func removeResponseCache(_ key: String) -> Bool @discardableResult func removeAllResponseCache() -> Bool 複製程式碼
再來看看MMCache.CacheContainer
enum CacheContainer { case RAM // 只緩存於記憶體的容器 case hybrid // 緩存於記憶體與磁碟的容器 } 複製程式碼
這兩種容器互不相通,即 即使key相同,使用 hybrid
來快取後,再通過 RAM
取值是取不到的。
- RAM : 僅緩存於記憶體之中,快取的資料在APP使用期間一直存在
- hybrid :緩存於記憶體與磁碟中,APP重啟後也可以獲取到資料
二、快取網路請求
內部快取過程:
- APP首次啟動並進行網路請求,網路資料將快取起來
- APP再次啟動並進行網路請求時,會先返回快取的資料,等請求成功後再返回網路資料
- 其它情況只會載入網路資料
- 每次成功請求到資料後,都會對快取的資料進行更新
// Normal func cacheRequest( _ target: Target, cacheType: MMCache.CacheKeyType = .default, callbackQueue: DispatchQueue? = nil, progress: Moya.ProgressBlock? = nil, completion: @escaping Moya.Completion ) -> Cancellable // Rx func cacheRequest( _ target: Base.Target, callbackQueue: DispatchQueue? = nil, cacheType: MMCache.CacheKeyType = .default ) -> Observable<Response> 複製程式碼
實際上是對 Moya
請求後的 Response
進行快取。 其實與 Moya
自帶的方法相比較只多了一個引數 cacheType: MMCache.CacheKeyType
,定義著快取中的 key
,預設為 default
下面是 MMCache.CacheKeyType
的定義
/** let cacheKey = [method]baseURL/path - default : cacheKey + "?" + parameters - base : cacheKey - custom : cacheKey + "?" + customKey */ public enum CacheKeyType { case `default` case base case custom(String) } 複製程式碼
如果你想快取 多頁
列表資料的 最新一頁
資料,此時使用 default
是不合適的,因為 default
使用的 key
包含了 pageIndex
,這樣就達不到只快取 最新一頁資料
的目的, 所以這裡應該使用 base
或者 custom(String)
我們可以來試一下帶快取的請求
/* * APP第一次啟動並進行網路請求,網路資料將快取起來 * APP再次啟動並進行網路請求時,會先載入快取,再載入網路資料 * 其它情況只會載入網路資料 * 每次成功請求到資料都會進行資料更新 */ lxfNetTool.rx.cacheRequest(.data(type: .all, size: 10, index: 1)) .subscribe(onNext: { response in log.debug("statusCode -- \(response.statusCode)") }).disposed(by: disposeBag) // 傳統方式 /* let _ = lxfNetTool.cacheRequest(.data(type: .all, size: 10, index: 1)) { result in guard let resp = result.value else { return } log.debug("statusCode -- \(resp.statusCode)") } */ 複製程式碼
列印結果
// 首次使用APP statusCode -- 200 // 關閉並重新開啟APP,再請求一下 statusCode -- 230 statusCode -- 200 // 然後再請求一下 statusCode -- 200 複製程式碼
這裡的 230
就是 MMStatusCode.cache.rawValue