打造Moya便捷解析庫,提供RxSwift擴充

LinXunFeng發表於2019-03-04

一、概述

1、相信大家在使用Swift開發時,Moya是首選的網路工具,在模型解析這一塊,Swift版模型解析的相關第三方庫有很多,本人最習慣用的就是SwiftyJSON

2、下面會開始講解整個主要的開發功能與思想。

3、以下內容是基於大家會使用Moya和SwiftJSON的前提下所著,還不會的同學可以先簡單瞭解後再來閱讀本篇文章哦~

二、功能開發與思想講解

1、嘗試模型解析

Moya請求伺服器返回的資料以Response類返回給我們,那我們就給Response類做一個擴充套件,這裡以解析模型為例

// 需要傳入一個引數,告知我們要轉換出什麼模型
public func mapObject<T: Modelable>(_ type: T.Type) -> T {
    // 模型解析過程
    。。。
  
    return T
}
複製程式碼

Q: 那中間的解析過程該怎麼寫呢?

A: 可以讓開發者遵守某個協議,實現指定的轉換方法並描述轉換關係。其轉換過程我們不需要知道,交給開發者即可。

那接著我們來定義一個協議Modelable,並宣告轉換方法

public protocol Modelable {
    mutating func mapping(_ json: JSON)
}
複製程式碼

開發者建立一個MyMoel的結構體,遵守協議Modelable,並實現mapping,書寫轉換關係

struct MyModel: Modelable {
    var _id = ""
    
    mutating func mapping(_ json: JSON) {
        self._id = json["_id"].stringValue
    }
}
複製程式碼

以目前的現狀來分析一下:mapObject可以讓開發者傳入模型型別,而我們的協議方法卻並非是個類方法。那我們需要先得到這個模型型別的物件,再來呼叫mapping方法

2、模型解析的驅動開發

Q: 怎麼得到這個物件?

A: 可以在協議中宣告一個初始化方法來建立物件。是的,我們在mapObject中建立對應模型型別的物件,呼叫mapping方法來轉換資料,再把模型物件傳出去即可。

那我們在Modelable中宣告一個init方法,並傳入一個引數,區別於其它初始化方法

public protocol Modelable {
    mutating func mapping(_ json: JSON)
    init(_ json: JSON)
}
複製程式碼

OK,現在把mapObject方法補齊模型解析過程

public func mapObject<T: Modelable>(_ type: T.Type) -> T {
    let modelJson = JSON(data)["modelKey"]
    // 模型解析過程
    var obj = T.init(modelJson)
    obj.mapping(modelJson)
    return obj
}
複製程式碼

3、自定義解析鍵名

Q: 這樣是搞定解析了,但是網路請求回來的json格式錯綜複雜,有什麼辦法可以讓開發者來自行指定model對應的鍵名呢?

A: 嗯嗯,既然解析過程是在 Response 擴充套件裡操作的,那我們可以通過協議定義鍵名屬性,並且使用 Runtime 給Response動態新增一個屬性,來記錄遵守協議後的相應類名

public protocol ModelableParameterType {
    /// 請求成功時狀態碼對應的值
    static var successValue: String { get }
    /// 狀態碼對應的鍵
    static var statusCodeKey: String { get }
    /// 請求後的提示語對應的鍵
    static var tipStrKey: String { get }
    /// 請求後的主要模型資料的鍵
    static var modelKey: String { get }
}
複製程式碼
// MARK:- runtime
extension Response {
    private struct AssociatedKeys {
        static var lxf_modelableParameterKey = "lxf_modelableParameterKey"
    }
    var lxf_modelableParameter: ModelableParameterType.Type {
        get {
            let value = objc_getAssociatedObject(self, &AssociatedKeys.lxf_modelableParameterKey) as AnyObject
            guard let type = value as? ModelableParameterType.Type else { return NullParameter.self }
            return type
        } set {
            objc_setAssociatedObject(self, &AssociatedKeys.lxf_modelableParameterKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
複製程式碼

這裡有個坑:_SwiftValue問題 (獻上 參考連結)
如果我們儲存的不是OC物件,那麼objc_getAssociatedObject取出來的值的型別統統為_SwiftValue,直接as? ModelableParameterType.Type絕對是nil,需要在取出來後as AnyObject再轉換為其它型別才會成功~~

現在開發者就可以建立一個類來遵守ModelableParameterType協議,並自定義解析鍵名

struct NetParameter : ModelableParameterType {
    static var successValue: String { return "false" }
    static var statusCodeKey: String { return "error" }
    static var tipStrKey: String { return "errMsg" }
    static var modelKey: String { return "results" }
}
複製程式碼

4、外掛注入

Q: 厲害了,不過要在什麼時機下儲存這個自定義鍵名的NetParameter

A: 額,這個~~~ 哦,對了,可以通過Moya提供的外掛機制!

翻出Moya中的Plugin.Swift,找到這個process方法,看看方法說明。

/// 在結束之前,可以被用來修改請求結果
/// Called to modify a result before completion.
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
複製程式碼

那好,我們也做一個外掛MoyaMapperPlugin給開發者使用,在建立MoyaMapperPlugin時把自定義解析鍵名的型別傳進來

public struct MoyaMapperPlugin: PluginType {
    var parameter: ModelableParameterType.Type
    
    public init<T: ModelableParameterType>(_ type: T.Type) {
        parameter = type
    }
    
    // modify response
    public func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response, MoyaError> {
        _ = result.map { (response) -> Response in
            // 趁機新增相關資料 
            response.lxf_modelableParameter = parameter
            return response
        }
        return result
    }
}
複製程式碼

使用:開發者在建立MoyaProvider物件時,順便注入外掛。(OS: 這一步堪稱“注入靈魂”)

MoyaProvider<LXFNetworkTool>(plugins: [MoyaMapperPlugin(NetParameter.self)])
複製程式碼

5、總結

以上就是主要的踩坑過程了。模型陣列解析和指定解析也跟這些差不多的,這裡就不再贅述。本人已經將其封裝成一個開源庫 MoyaMapper,包含了上述已經和未曾說明的功能,下面會講解如何去使用。以上部分可以稱為開胃菜,目的就是平滑過渡到下面MoyaMapper的具體使用。

可能單單使用MoyaMapper的預設子庫Core,作用體會上並不會很深。但是,如果你也是使用RxSwift來開發專案的話,請安裝`MoyaMapper/Rx`吧,絕對一個字:「爽」

二、MoyaMapper的使用

MoyaMapper

MoyaMapper是基於Moya和SwiftyJSON封裝的工具,以Moya的plugin的方式來實現間接解析,支援RxSwift

JSON資料對照

1、定義並注入自定義鍵名類

  1. 定義一個遵守ModelableParameterType協議的結構體
// 各引數返回的內容請參考上面JSON資料對照圖
struct NetParameter : ModelableParameterType {
    static var successValue: String { return "false" }
    static var statusCodeKey: String { return "error" }
    static var tipStrKey: String { return "" }
    static var modelKey: String { return "results" }
}
複製程式碼

此外,這裡還可以做簡單的路徑處理,以應付各種情況,以`>`隔開

// 假設返回的json資料關於請求狀態的相關資料如下所示,
error: {
    `errorStatus`:false
    `errMsg`:`error Argument type`
}

複製程式碼
// 我們指明解析路徑:error物件下的errMsg欄位,一層層表示下去即可
static var tipStrKey: String { return "error>errMsg" }
複製程式碼
  1. 以plugin的方式傳遞給MoyaProvider
// MoyaMapperPlugin這裡只需要傳入型別
MoyaProvider<LXFNetworkTool>(plugins: [MoyaMapperPlugin(NetParameter.self)])
複製程式碼

2、定義解析模型

建立一個遵守Modelable協議的結構體

struct MyModel: Modelable {
    
    var _id = ""
    ...
    
    init(_ json: JSON) { }
    
    mutating func mapping(_ json: JSON) {
        self._id = json["_id"].stringValue
        ...
    }
}

遵守Modelable協議,實現協議的兩個方法,在`mapping`方法中描述模型欄位的具體解析

複製程式碼

3、解析資料

0x00 請求結果與模型解析

// Result
public func mapResult(params: ModelableParamsBlock? = nil) -> MoyaMapperResult

// Model
public func mapObject<T: Modelable>(_ type: T.Type, modelKey: String? = nil) -> T
// Result+Model
public func mapObjResult<T: Modelable>(_ type: T.Type, params: ModelableParamsBlock? = nil) -> (MoyaMapperResult, T)

// Models
public func mapArray<T: Modelable>(_ type: T.Type, modelKey: String? = nil) -> [T]
// Result+Models
public func mapArrayResult<T: Modelable>(_ type: T.Type, params: ModelableParamsBlock? = nil) -> (MoyaMapperResult, [T])
複製程式碼

上面的五個方法,觀其名,知其意,這裡就不過多解釋了,主要注意兩點:

  • result
// 元祖型別
// 引數1:根據statusCodeKey取出的值與successValue是否相等
// 引數2:根據tipStrKey取出的值
result:(Bool, String)
複製程式碼
  • params
// params: ModelableParamsBlock? = nil
// 這裡只有在特殊場景下才需要使用到。如:專案中需要在某處使用特定介面,但是返回的json格式跟自己專案的不一樣,並且只有這麼一兩處用得著該額外介面,那就需要我們這個引數了,以Block的方式返回解析引數型別。
複製程式碼

0x01、特定解析

// Model
public func toJSON(modelKey: String? = nil) -> JSON
// 獲取指定路徑的值
public func fetchJSONString(path: String? = nil, keys: [JSONSubscriptType]) -> String
複製程式碼

這兩個方法,如果沒有指定路徑,預設都是針對modelKey的

// fetchJSONString(keys: <[JSONSubscriptType]>)
1、通過 keys 傳遞陣列, 該陣列可傳入的型別為 IntString
2、預設是以 modelKey 所示路徑,來獲取相應的數值。如果modelKey並非是你所想要使用的解析路徑,可以使用下方的過載方法重新指定路徑即可

// response.fetchJSONString(path: <String?>, keys: <[JSONSubscriptType]>)
複製程式碼

MoyaMapper也提供了Rx子庫,為方便RxSwift的流式程式設計下便捷解析資料

MoyaMapper預設只安裝Core下的檔案
pod `MoyaMapper`

RxSwift擴充
pod `MoyaMapper/Rx`
複製程式碼

具體使用還不是很明白的同學可以下載並執行Example看看

如果MoyaMapper有什麼不足的地方,歡迎提出issues,感謝大家的支援

相關文章