Swift KVC再一次實現(xiàn)JSON轉(zhuǎn)MODEL

摘要

本文描述了一種適用于Swift語言的JSON與MODEL互轉(zhuǎn)方法见咒,
這里不涉及復(fù)雜Decode機制(Codable)和不安全的內(nèi)存訪問(HandyJSON)现使。
僅使用Swift的KVC和Macro達到目的荐绝。

從KVC開始

假設(shè)存在一個struct類型Person和對應(yīng)結(jié)構(gòu)的JSON:

struct Person {
  var name = ""
  var age = 0
}
{
  "name": "HelloWorld",
  "age": 18
}

已知Swift是有KVC的(個人猜測不走runtime)一汽,通過類型安全的KeyPath可以實現(xiàn)屬性的讀寫,例如:

var person = Person()
person[\Person.name] = "Hello"

其中的手寫的表達式\Person.name就是WritableKeyPath<Person, String>類型的一個keyPath,關(guān)于KVC可以參考官網(wǎng)介紹:Swift KVC

要從json的"name"讀出"HelloWorld"并寫入Person對象的name屬性中召夹,我們可以直接走以下步驟:

var per = Person()

let nameFromJson = json["name"] // any
let nameForPerson = nameFromJson as? String // 把any轉(zhuǎn)成String
if let nameForPerson = nameForPerson {
  per[keyPath: \Person.name] = nameForPerson // 通過kvc寫入
}

反過來從Person對象讀取name屬性寫入json:

var json = [String: Any]()
let nameFromPerson = person[keyPath: \Person.name] // 通過kvc讀出String
let nameForJSON = nameFromPerson as Any
json["name"] = nameForJSON

以上兩個事例可以完成Person對象的name屬性與JSON的"name"相互轉(zhuǎn)化了岩喷,那么剩下的age屬性也按照這個套路走就行了。簡單的封裝一下监憎,可以滿足各種方法纱意。

把以下4個元素整合起來:

  • key :例如上面JSON的"name"
  • keyPath :例如上面Person對象的\Person.name
  • JSON Any轉(zhuǎn)化為屬性類型,并寫入MODEL的方法
  • 屬性類型轉(zhuǎn)化為JSON Any鲸阔,并寫入JSON的方法 (這一步就不細講了偷霉,請讀者舉一反三)
struct JSONKeyPathObject<Model> {
  let key: String
  let keyPath: PartialKeyPath<Model>
  var setJsonValueToModel: (Any, inout Model) -> Void
  // var setModelValueToJson: (Any, inout [String: Any]) -> Void // (這一步就不細講了,請讀者舉一反三)
}

從String褐筛、Int类少、Double、Bool等基本數(shù)據(jù)類型開始

很顯然渔扎,基本數(shù)據(jù)類型都可以提前設(shè)定好setJsonValueToModel方法硫狞,例如int、string晃痴、double残吩、bool直接走這一個初始化方法,并且方法的泛型Value就是KeyPath里的Value:

extension JSONKeyPathObject {
    init<Value>(key: String, keyPath: WritableKeyPath<Model, Value>) where Value: BasicDataType {
        self.key = key
        self.keyPath = keyPath
        self.setJsonValueToModel = { anyJsonValue, model in
            guard let value = anyJsonValue as? Value else {
                return
            }
            model[keyPath: keyPath] = value
        }
    }
}

protocol BasicDataType {
}

extension String: BasicDataType {}
extension Int: BasicDataType {}
extension Double: BasicDataType {}
extension Bool: BasicDataType {}

把Person內(nèi)的所有屬性組成JSONKeyPathObject數(shù)組倘核,遍歷完不就直接把JSON轉(zhuǎn)化完了嗎世剖?!

補充一下Person的內(nèi)容

struct Person {
    var name = ""
    var age = 0
    
    func allKeyPathObjects() -> [JSONKeyPathObject<Person>] {
        return [
            .init(key: "name", keyPath: \Person.name),
            .init(key: "age", keyPath: \Person.age),
        ]
    }
}

遍歷數(shù)組笤虫,給person對象寫入屬性值

let json: [String: Any] = [
    "name": "HelloWorld",
    "age": 18,
]

var person = Person()
for keyPathObject in person.allKeyPathObjects() {
    keyPathObject.setJsonValueToModel(json[keyPathObject.key], &person)
}
print(person)

打印結(jié)果如下,復(fù)合預(yù)期

Person(name: "HelloWorld", age: 18)

這樣的散裝代碼還是再封裝一下比較好用祖凫,直接定義一個新協(xié)議琼蚯,就叫JSONable吧,必須實現(xiàn)allKeyPathObjects方法惠况,以及decodeFromJSON和encodeToJSON遭庶。(同樣的encodeToJSON不做詳細介紹,可以舉一反三)

protocol JSONable {
    func allKeyPathObjects() -> [JSONKeyPathObject<Self>]
    mutating func decodeFromJSON(_ json: [String: Any])
}

extension JSONable {
    mutating func decodeFromJSON(_ json: [String: Any]) {
        for keyPathObject in allKeyPathObjects() {
            if let jsonValue = json[keyPathObject.key] {
                keyPathObject.setJsonValueToModel(jsonValue, &self)
            }
        }
    }
}

遞歸子MODEL轉(zhuǎn)化

既然我們有了JSONable協(xié)議了稠屠,是不是再給JSONKeyPathObject拓展一個新的初始化方法就行了峦睡?!也就可以實現(xiàn)遞歸子MODEL了权埠?榨了?!

是的攘蔽,直接上手

extension JSONKeyPathObject
    init<Value>(key: String, keyPath: WritableKeyPath<Model, Value>) where Value: JSONable {
        self.key = key
        self.keyPath = keyPath
        self.setJsonValueToModel = { anyJsonValue, model in
            guard let json2 = anyJsonValue as? [String: Any] else {
                return
            }
            var newSubModel = Value()
            newSubModel.decodeFromJSON(json2)
            model[keyPath: keyPath] = newSubModel
        }
    }
}

因為泛型Value要創(chuàng)建實例Value()龙屉,所以給協(xié)議JSONable補充一個init()方法

protocol JSONable {
    init()
    func allKeyPathObjects() -> [JSONKeyPathObject<Self>]
    mutating func decodeFromJSON(_ json: [String: Any])
}

寫一個嵌套模型試一下,給Person補充一個屬性Pet满俗,也是JSONable的

struct Person: JSONable {
    var name = ""
    var age = 0
    var pet = Pet()
    
    func allKeyPathObjects() -> [JSONKeyPathObject<Person>] {
        return [
            .init(key: "name", keyPath: \Person.name),
            .init(key: "age", keyPath: \Person.age),
            .init(key: "pet", keyPath: \Person.pet),
        ]
    }
    
    struct Pet: JSONable {
        var type = ""
        var weight = 0
        
        func allKeyPathObjects() -> [JSONKeyPathObject<Pet>] {
            return [
                .init(key: "type", keyPath: \Pet.type),
                .init(key: "weight", keyPath: \Pet.weight),
            ]
        }
    }
}

let json: [String: Any] = [
    "name": "HelloWorld",
    "age": 18,
    "pet": [
        "type": "dog",
        "weight": 20,
    ],
]

var person = Person()
person.decodeFromJSON(json)
print(person)

打印結(jié)果正常

Person(name: "HelloWorld", age: 18, pet: MyJSONableLite.Person.Pet(type: "dog", weight: 20))

以上就把遞歸子MODEL也賦值上去了

Array转捕、Dictionary怎么辦作岖?Optional包裝類型又怎么辦

很簡單,再拓展JSONKeyPathObject五芝,以Array<JSONable>舉例

extension JSONKeyPathObject {
    init<Value>(key: String, keyPath: WritableKeyPath<Model, Array<Value>>) where Value: JSONable {
        self.key = key
        self.setJsonValueToModel = { anyJsonValue, model in
            guard let jsonArray = anyJsonValue as? [[String: Any]] else {
                return
            }
            model[keyPath: keyPath] = jsonArray.map { json2 in
                var newSubModel = Value()
                newSubModel.decodeFromJSON(json2)
                return newSubModel
            }
        }
    }
}

其他的都差不多痘儡,改寫一下WritableKeyPath類型和setJsonValueToModel方法即可,可以舉一反三枢步。

從Macro結(jié)束

到這里讀者們肯定要反駁:這個要手寫allKeyPathObjects方法沉删,太麻煩了,要手寫key字符串价捧,寫錯了就出大bug了丑念,我才不想用。

我想說:且慢结蟋!還有一計脯倚!

從Swift 5.9開始(Xcode15),也有Macro的支持了嵌屎,借助宏的力量推正,可以幫我們自動補充allKeyPathObjects代碼!

反復(fù)研究我們的MODEL定義宝惰,發(fā)現(xiàn)只要拿到以下元素植榕,就可以組裝我們的allKeyPathObjects代碼

  • MODEL的名稱,例如Person
  • 每一個Property的名稱尼夺,例如name
  • 拼裝KeyPath尊残,例如\Person.name
struct Person: JSONable {
    var name = ""
    var age = 0
    
    func allKeyPathObjects() -> [JSONKeyPathObject<Person>] {
        return [
            .init(key: "name", keyPath: \Person.name),
            .init(key: "age", keyPath: \Person.age),
        ]
    }
}

宏的實現(xiàn)比較復(fù)雜,可以直接查看成品Github地址淤堵,等后續(xù)將會補充part2專門研究這個宏怎么寫的

最終在最前面標(biāo)記我們的宏寝衫,讓編譯器完成代碼插入

@JSONableMacro
struct Person: JSONable {
    var name = ""
    var age = 0
}

完結(jié)

以上就是KVC實現(xiàn)的JSON轉(zhuǎn)MODEL方法,使用Macro更近一步減少手寫的代碼量拐邪。
有興趣的讀者還可以進一步實現(xiàn)Enum慰毅、Date等類型的轉(zhuǎn)化方法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市扎阶,隨后出現(xiàn)的幾起案子汹胃,更是在濱河造成了極大的恐慌,老刑警劉巖东臀,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件着饥,死亡現(xiàn)場離奇詭異,居然都是意外死亡惰赋,警方通過查閱死者的電腦和手機贱勃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贵扰,你說我怎么就攤上這事仇穗。” “怎么了戚绕?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵纹坐,是天一觀的道長。 經(jīng)常有香客問我舞丛,道長耘子,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任球切,我火速辦了婚禮谷誓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吨凑。我一直安慰自己捍歪,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布鸵钝。 她就那樣靜靜地躺著糙臼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪恩商。 梳的紋絲不亂的頭發(fā)上变逃,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音怠堪,去河邊找鬼揽乱。 笑死,一個胖子當(dāng)著我的面吹牛粟矿,可吹牛的內(nèi)容都是我干的锤窑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼嚷炉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了探橱?” 一聲冷哼從身側(cè)響起申屹,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隧膏,沒想到半個月后哗讥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡胞枕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年杆煞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡决乎,死狀恐怖队询,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情构诚,我是刑警寧澤蚌斩,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站范嘱,受9級特大地震影響送膳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丑蛤,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一叠聋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧受裹,春花似錦碌补、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至砰盐,卻和暖如春闷袒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岩梳。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工囊骤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冀值。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓也物,卻偏偏與公主長得像,于是被迫代替她去往敵國和親列疗。 傳聞我的和親對象是個殘疾皇子滑蚯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容