教你如何用Swift寫個json轉模型的開源庫

在iOS項目開發過程當中,咱們常常會用到將從服務器獲取的 json 轉 model 的操做,咱們可使用 Swift 提供的setValuesForKeys 或者 Objective-C 提供的setValuesForKeysWithDictionary 方法來完成這一操做。git

使用上面兩個方法只能將字典轉換成 model , 若是 json 最外層是個數組,那麼咱們就必須在循環中使用這個方法,這很是不方便, 並且還有個條件,就是 model 中的全部屬性名必須跟字典中的 key 徹底對應,這樣就會遇到另一個問題,若是咱們字典中的一個 key 與系統關鍵字重名,那咱們在 model 就不能使用這個 key 做爲屬性名了。github

爲了解決上面的問題,咱們會使用一些第三方庫去完成字典轉模型的操做,例如 MJExtension 。因爲它是一個 OC 的庫,因此在 Swift 項目中須要引入橋接文件。在 Swift 中使用其 API 時實際上是很不 swift 的。因此如今咱們就用 Swift 3.0 來寫一個 swift style 的 json 轉模型的庫吧。json

例如咱們有這樣的兩個 model:swift

class User: NSObject {
    var name: String?
    var age = 0
    var desc: String?
}
class Repos: NSObject {
    var title: String?
    var owner: User?
    var viewers: [User]?
}

最終咱們想實現這樣的調用:數組

let repos = json ~> Repos.self    // 將一個字典轉成一個Repos的實例
 
let viewers  = viewers => User.self  //將一個數組轉換成User的數組

~>=> 是自定義的運算符,主要是爲了調用方便。它們的定義是這樣的:服務器

public func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T?
public func =><T: NSObject>(lhs: Any, rhs: T.Type) -> [T]?

這裏給出個人實現 ModelSwift。你們能夠先看看個人實現而後試着寫出本身的實現。好了,如今就讓咱們開始吧。數據結構

要解決的問題

因爲將數組轉成模型數組,其實要作的工做跟將字典轉模型是同樣的,只是作了個循環而已。因此咱們首先要解決的問題是:如何在 Swift 將字典轉成模型。這裏咱們是使用 KVC就能夠了。咱們使用 NSObject 的 setValue(_ value: Any?, forKey key: String) 方法來給對象設置值。ide

從上面要實現的效果來看,咱們在使用前並不用先實例化一個對象。因此咱們要解決的第二個問題是:如何經過類型來實例化一個對象。 測試

另外一個要解決的問題是字典中的 key 與關鍵字重名,或者咱們想使用本身的名字。因此咱們要實現本身的映射的策略。ui

還有一個問題是,若是咱們服務器返回的字典數據中包含另一個字典數組,對應咱們的 model 中就是一個對象包含另一個對象的數組。那麼咱們怎樣才能知道這個數組中對象的類型呢?

實現思路

對於上面提到的第一問題我在上面已經給出瞭解決方案,就是讓咱們的 model 繼承 NSObject, 而後使用 setValue(_ value: Any?, forKey key: String) 方法來給對象設置值。這裏的 value 實際上是要根據 model 中的屬性名去字典中獲取的。若是咱們能拿到 model 全部的屬性名,那麼給 model 設置值的操做就完成了。那麼如何獲取到 model 的屬性名呢?這就必須得使用到 Swift 中的反射機制了。

Mirror

Swift 的反射機制是基於一個叫 Mirror 的 struct 來實現的。對於 Mirror 的詳細結構你們能夠按住 cmd 點進去查看。這裏咱們主要關注的是 public typealias Child = (label: String?, value: Any) 這個 typealias,它實際上是一個元祖,label 就表示咱們的屬性名,是 Optional 的。value 表示的是屬性的值。這裏 label 爲何是 Optional 的?若是你仔細考慮下,其實這是很是有意義的,並非全部支持反射的數據結構都包含有名字的子節點。 Mirror 會以屬性的名字作爲 label,可是 Collection 只有下標,沒有名字。Tuple 一樣也可能沒有給它們的條目指定名字。

Mirror 有個 children 的存儲屬性,它的定義是這樣的:

public let children: Mirror.Children

這裏的 Mirror.Children 也是一個 typealias,它是這樣定義的:

public typealias Children = AnyCollection<Mirror.Child>

能夠看到它是 Child 的集合。因此咱們能夠經過 Mirror 的 children 屬性來得到 model 的全部屬性名。

咱們寫個類來測試一下:

class Person: NSObject {
    var name = ""
    var age = 0
    var friends: [Person]?
}

let mirror = Mirror(reflecting: Person())
for case let (label?, value) in mirror.children {
    print ("\(label) = \(value)")
}

運行結果是以下:

name = 
age = 0
friends = nil

Mirror 還有一個類型爲 Any.TypesubjectType 存儲屬性,表示該映射對象的類型,例如上面的 mirror.subjectType 就是 User。使用 subjectType 就能夠得到對象的類型以及其全部屬性的類型。爲了實現這個效果,咱們能夠寫出下面的代碼:

func subjectType(of subject: Any) -> Any.Type {
    let mirror = Mirror(reflecting: subject)
    return mirror.subjectType
}

func children(of subject: Any) {
    let mirror = Mirror(reflecting: subject)
    for case let(label?, value) in mirror.children {
        print ("\(label) = \(subjectType(of: value))")
    }
}

children(of: Person())

打印結果是這樣的:

name = String
age = Int
friends = Optional<Array<Person>>

我本來想使用這個方法來獲得 model 中包含的另外對象的類型和數組中對象的類型,例如 Person 中有 fatherfriends 屬性:

class Person: NSObject {
    var name = ""
    var age = 100
    var father: Person?
    var friends: [Person]?
}

可是發現結果是 Optional<Person>Optional<Array<Person>>。因此咱們仍是得顯示地指出一個 model 中包含的其餘對象的類型,以及數組中對象的類型。在後面我會給出本身的實現。你們能夠給出本身的實現。

經過類型來實例化一個對象

要使用 Mirror 來得到反射對象的全部屬性名,就必須先使用 init(reflecting subject: Any) 來建立一個 Mirror。而建立 Mirror 就必須傳入一個 subject(在這裏咱們主要傳入一個NSObject類型的對象)。因此咱們的首要任務就是經過類型來實例化一個對象。

有些同窗可能有疑問了:我要轉換成 Person 的對象,我直接傳入一個
Person 的實例就好了啊。若是你看看咱們 josn 轉模型的方法定義就能明白了。 func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T?

仍是以上面的 Person 爲例,咱們看看這樣的調用:

Person.self().age
// 結果是:100

因此咱們經過一個類的 self()方法能夠獲得一個類的實例。其實咱們還能夠經過 AnyClass 來實例化對象。AnyClass 是類的類型,其定義是這樣的:

public typealias AnyClass = AnyObject.Type

咱們經過類的self屬性能夠獲得類的類型:

Person.self     
//結果是:Person.Type

獲得類的類型後,經過調用其 init()方法就能夠建立一個實例了:

Person.self.init().age
// 結果是:100

使用類型建立對象的類中的init方法前面必須是 required 的,由於這麼建立方式是使用meta type來建立的。因爲咱們 json 轉模型的 model 是繼承自 NSObject 的,因此不用在每一個類中顯示地實現。

寫個簡單的 josn 轉模型

有了上面的基礎就能夠來實現咱們的 josn 轉模型了。首先咱們來寫出 ~> 的定義,並經過類來建立一個對象

infix operator ~>

func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T? {
    guard let json = lhs as? [String: Any], !json.isEmpty else {
        return nil
    }
    
    let obj = T.self()
    let mirror = Mirror(reflecting: obj)
    
    for case let(label?, value) in mirror.children {
        print ("\(label) = \(value)")
    }
    
    return obj
}

class Person: NSObject {
    var name = ""
    var age = 0

    override var description: String {
        return "name = \(name), age = \(age)"
    }
}
let json: [String: Any] = ["name": "jewelz", "age": 100]
let p = json ~> Person.self
// 打印結果:
// name = 
// age = 0

經過上面的幾行代碼咱們確實成功的建立了一個 Person 的實例了。下一步就是給實例設置值了。咱們在上面的 for 循環中添加以下代碼:

// 從字典中獲取值
if let value = json[label] {
     obj.setValue(value, forKey: label)
}

整個代碼就是這樣的:

infix operator ~>

func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T? {
    guard let json = lhs as? [String: Any], !json.isEmpty else {
        return nil
    }
    
    let obj = T.self()
    let mirror = Mirror(reflecting: obj)
    
    for case let(label?, _) in mirror.children {
        // 從字典中獲取值
        if let value = json[label] {
            obj.setValue(value, forKey: label)
        }
    }
    return obj
}

let p = json ~> Person.self
print(p!)
//結果:name = jewelz, age = 100

有了上面 ~> 的實現,=> 的實現就很簡單了:

infix operator =>
func =><T: NSObject>(lhs: Any, rhs: T.Type) -> [T]? {
    guard let array = lhs as? [Any], !array.isEmpty else {
        return nil
    }
    
    return array.flatMap{ $0 ~> rhs }
}

上面只是實現了一個簡單 josn 轉模型,其實在實際項目中要解決的問題還有不少。如今再來看看我在文章開頭給出的 User 類和 Respo 類:

class User: NSObject {
    var name: String?
    var age = 0
    var desc: String?
}
class Repos: NSObject {
    var title: String?
    var owner: User?
    var viewers: [User]?
}

只簡單的用上面的實現是沒法獲得想要的結果的。對於 User 類來講,desc 屬性對應 json 的 description key,因此咱們還要進行 model 的屬性與 json 的鍵的映射。這裏的思路就是將 model 的屬性名做爲 key,以要替換的 json 的鍵做爲 value 存入字典中。咱們能夠拓展 NSObject ,添加一個計算屬性並提供一個空實現。不過這樣的傾入性太大,畢竟不是全部的類都須要作這個映射。因此最後的方式是 POP。好比咱們能夠制定這樣一個協議:

public protocol Reflectable: class {
    var reflectedObject: [String: Any.Type] { get }
}

在須要作映射的類中去實現該協議。

對於更復雜的 Repos 類來講,要作的事情更多。好比 owner的類型怎麼知道?owner 這個對象怎麼完成賦值?viewers 數組中的類型是什麼,怎樣才能完成賦值? 雖然經過上面提到的 Mirro 能夠獲得全部的類型,但獲得的是 Optional<User>以及 Optional<Array<User>>。個人解決的辦法就跟上面作屬性名替換是同樣的。這裏就不詳細地說明了,你們能夠各顯神通。寫出本身的實現。

寫在最後

經過上面的幾個步驟,咱們就能很快的實現一個簡單的 json 轉模型的需求了。總結起來就是如下幾點:

  • 全部要轉換的 model 繼承 NSObject

  • 使用類的類型來實例化對象

  • 經過反射得到對象的全部屬性名

  • 經過 setValue(_ value: Any?, forKey key: String) 方法來給屬性設置值

對於在最後提出的幾個問題,我這裏就不一一詳細地說明了。你們能夠點這裏看看個人實現。你們可使用 CocoaPods 或者 Carthage 將 ModelSwift 集成到項目中。若是在使用中有什麼問題能夠 issue 我,也能夠給個 star 持續關注。

相關文章
相關標籤/搜索