摘要
本文描述了一种适用于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等类型的转化方法
网友评论