美文网首页
Swift KVC再一次实现JSON转MODEL

Swift KVC再一次实现JSON转MODEL

作者: zjam9333 | 来源:发表于2024-08-24 22:48 被阅读0次

摘要

本文描述了一种适用于Swift语言的JSON与MODEL互转方法,
这里不涉及复杂Decode机制(Codable)和不安全的内存访问(HandyJSON)。
仅使用Swift的KVC和Macro达到目的。

从KVC开始

假设存在一个struct类型Person和对应结构的JSON:

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

已知Swift是有KVC的(个人猜测不走runtime),通过类型安全的KeyPath可以实现属性的读写,例如:

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

其中的手写的表达式\Person.name就是WritableKeyPath<Person, String>类型的一个keyPath,关于KVC可以参考官网介绍:Swift KVC

要从json的"name"读出"HelloWorld"并写入Person对象的name属性中,我们可以直接走以下步骤:

var per = Person()

let nameFromJson = json["name"] // any
let nameForPerson = nameFromJson as? String // 把any转成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"相互转化了,那么剩下的age属性也按照这个套路走就行了。简单的封装一下,可以满足各种方法。

把以下4个元素整合起来:

  • key :例如上面JSON的"name"
  • keyPath :例如上面Person对象的\Person.name
  • JSON Any转化为属性类型,并写入MODEL的方法
  • 属性类型转化为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等基本数据类型开始

很显然,基本数据类型都可以提前设定好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内的所有属性组成JSONKeyPathObject数组,遍历完不就直接把JSON转化完了吗?!

补充一下Person的内容

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

遍历数组,给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)

打印结果如下,复合预期

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

这样的散装代码还是再封装一下比较好用,直接定义一个新协议,就叫JSONable吧,必须实现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转化

既然我们有了JSONable协议了,是不是再给JSONKeyPathObject拓展一个新的初始化方法就行了?!也就可以实现递归子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要创建实例Value(),所以给协议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)

打印结果正常

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结束

到这里读者们肯定要反驳:这个要手写allKeyPathObjects方法,太麻烦了,要手写key字符串,写错了就出大bug了,我才不想用。

我想说:且慢!还有一计!

从Swift 5.9开始(Xcode15),也有Macro的支持了,借助宏的力量,可以帮我们自动补充allKeyPathObjects代码!

反复研究我们的MODEL定义,发现只要拿到以下元素,就可以组装我们的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),
        ]
    }
}

宏的实现比较复杂,可以直接查看成品Github地址,等后续将会补充part2专门研究这个宏怎么写的

最终在最前面标记我们的宏,让编译器完成代码插入

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

完结

以上就是KVC实现的JSON转MODEL方法,使用Macro更近一步减少手写的代码量。
有兴趣的读者还可以进一步实现Enum、Date等类型的转化方法

相关文章

网友评论

      本文标题:Swift KVC再一次实现JSON转MODEL

      本文链接:https://www.haomeiwen.com/subject/vuknkjtx.html