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

huluobo發表於2017-03-26

在iOS專案開發過程中,我們經常會用到將從伺服器獲取的 json 轉 model 的操作,我們可以使用 Swift 提供的setValuesForKeys 或者 Objective-C 提供的setValuesForKeysWithDictionary 方法來完成這一操作。

使用上面兩個方法只能將字典轉換成 model , 如果 json 最外層是個陣列,那麼我們就必須在迴圈中使用這個方法,這非常不方便, 而且還有個條件,就是 model 中的所有屬性名必須跟字典中的 key 完全對應,這樣就會遇到另外一個問題,如果我們字典中的一個 key 與系統關鍵字重名,那我們在 model 就不能使用這個 key 作為屬性名了。

為了解決上面的問題,我們會使用一些第三方庫去完成字典轉模型的操作,例如 MJExtension 。由於它是一個 OC 的庫,所以在 Swift 專案中需要引入橋接檔案。在 Swift 中使用其 API 時其實是很不 swift 的。所以現在我們就用 Swift 3.0 來寫一個 swift style 的 json 轉模型的庫吧。

例如我們有這樣的兩個 model:

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) 方法來給物件設定值。

從上面要實現的效果來看,我們在使用前並不用先例項化一個物件。所以我們要解決的第二個問題是:如何通過型別來例項化一個物件。

另一個要解決的問題是字典中的 key 與關鍵字重名,或者我們想使用自己的名字。所以我們要實現自己的對映的策略。

還有一個問題是,如果我們伺服器返回的字典資料中包含另外一個字典陣列,對應我們的 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 持續關注。

相關文章