原文链接:https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift
在iOS中最常见的工作是将数据保存起来并通过网络传输。但是在这之前,你需要将数据通过编码
或序列化
转换成合适的格式。
同样的,在你使用这些数据之前,你也需要将其转换成合适的格式。这个相反的过程被称为解码
或反序列化
。
在这个教程中,你将学习到所有使用Swift进行编解码所需要的知识。包括这些:
- 在
蛇形命名
和驼峰命名
格式之间转换 - 自定义
Coding keys
- 使用
keyed
,unkeyed
和nested
容器 - 处理
嵌套类型
,日期类型
以及子类
这确实有点多,是时候开始动手了!
开始动手
从这里下载所需资源后继续(先复制提取码lgnb
)。
下载完成后,starter是该教程使用的版本。final是最终完成的版本。
我们打开本节代码Nested types
。使Toy
和Employee
遵循Codable
协议:
1 |
|
Codable
本身并不是一个协议,它只是另外两个协议的别名:Encodable
和Decodable
。你也行已经猜到了,这两个协议就是代表那些可以被编解码的类型。
你无需再做其他事情,因为Toy
和Employee
的所有存储属性都是Codable
的。Swift标准库中大多数类型(例如String
、URL
)都是支持Codable
的。
添加一个JSONEncoder
和JSONDecoder
来处理toys
和employees
的编解码:
1 |
|
操作JSON我们只需做这些!下面进入第一个挑战!
编解码嵌套类型
Employee
包含了一个Toy
属性(这是个嵌套类型)。编码后的JSON结构和Employee
结构体保持一致:
1 |
|
1 |
|
JSON数据将name
嵌套在favoriteToy
之中,并且所有的JSON字段名与Toy
和Employee
的存储属性名相同,所以基于结构体的类型体系,JSON的结构很容易理解。如果属性名称和JSON的字段名都相同,并且属性都是Codable
的,那么我们可以很容易的将JSON转换为数据模型,或者反过来。现在来试一试:
1 |
|
这里做了2件事:
- 将
employee
使用encode(_:)
编码成JSON。是不是很简单! - 从上一步的
data
中创建String,一遍可以查看其内容。
这里的编码过程会产生合法的数据,所以我们可以使用它重新创建employee
:
1 |
|
好了,可以开始下一个挑战了!
在蛇形命名
和驼峰命名
格式之间转换
现在,假设JSON的键名从驼峰格式(这样looksLikeThis
)转换成了蛇形格式(这样looks_like_this_instead
)。但是,Toy
和Employee
的存储属性只能使用驼峰格式。幸运的是Foundation
考虑到了这种情况。
打开本节代码Snake case vs camel case
,在编解码器创建之后使用之前的位置添加下面的代码:
1 |
|
运行代码,检查snakeString
,编码后的employee
产生下面的内容:
1 |
|
自定义Coding keys
现在,假设JOSN的格式再一次改变,其使用的字段名和Toy
和Employee
中存储属性名不一致了:
1 |
|
可以看到,这里使用gift
代替了原来的favoriteToy
。这种情况我们需要自定义Coding keys
。在我们的类型中添加一个特殊的枚举类型。打开本节代码Custom coding keys
,在Employee
中添加下面的代码:
1 |
|
这个特殊的枚举遵循了CodingKey
协议,并使用String
类型的原始值。在这里我们可以让favoriteToy
和gift
一一对应起来。
在编解码过程中,只会操作出现在枚举中的cases
,所以即使那些不需要指定一一对应的属性,也需要在枚举中包含,就像这里的name
和id
。
运行playground
,然后查看string
的值,你会发现JSON字段名不在依赖存储属性名称,这得益于自定义的Coding keys
。
继续下一个挑战!
处理扁平化
的JSON
现在,JSON的格式变成下面这样:
1 |
|
这里不在有嵌套
结构,和我们的模型结构不一致了。这种情况我们需要自定义编解码过程。
打开本节代码Keyed containers
。这里有个Employee
类型,它遵循了Encodable
。同时我们使用extension
让它遵循了Decodable
。这样做的好处是,可以保留结构体的逐一成员构造器
。如果我们在定义Employee
时让它遵循Decodable
,它将失去这个构造器。添加下面的代码到Employee
中:
1 |
|
在之前简单(指属性名和键名一一对应且嵌套层级相同)的示例中,encode(to:)
方法由编译器自动实现了。现在我们需要手动实现。
- 创建
CodingKeys
表示JSON的字段。因为我们没有做任何的关系映射,所以不必声明它的原始类型为String
。 - 从
encoder
中获取KeyedEncodingContainer
容器。这就像一个字典,我们可以存储属性的值到其中,这样就进行了编码。 - 编码
name
和id
属性到容器中。 - 使用
gift
键,直接将toy
的名字编码到容器中。
运行playground
,然后查看string
的值,你会发现它符合上面JSON的格式。我们可以选择使用什么字段名编码一个属性值,这给了我们很大的灵活性。
和编码过程类似,简单版本的init(from:)
方法可以由编译器自动实现。但是这里我们需要手动实现,使用下面的代码替换fatalError("To do")
:
1 |
|
然后添加下面的代码,就可以从JSON中重新创建employee
:
1 |
|
处理多级嵌套的JSON
现在,JSON的格式变成下面这样:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
name
字段在toy
字段中,而toy
又在gift
字段中。如何解析成我们定义的数据模型呢?
打开本节代码Nested keyed containers
,添加下面的代码到Employee
:
1 |
|
这里做了几件事:
- 创建顶层的
CodingKeys
- 创建用于解析
gift
字段的CodingKeys
,后续使用它创建容器 - 使用顶层容器编码
name
和id
- 使用
nestedContainer(keyedBy:forKey:)
方法获取用于编码gift
字段的容器,并将favoriteToy
编码进去 运行并查看string
的值,你会发现JSON的格式符合预期。
解码过程也很类似。添加下面的代码:
1 |
|
好了,我们已经搞定了嵌套类型的容器。并从其中解码出了sameEmployee
。
处理日期类型
现在,JSON里添加了日期字段,就像下面这样:
1 |
|
JSON中并没有标准的日期格式。在JSONEncoder
和JSONDecoder
使用日期类的timeIntervalSinceReferenceDate
方法去处理(Date(timeIntervalSinceReferenceDate: interval)
)。
这里我们需要指定日期转换策略
。打开本节代码Dates
,在try encoder.encode(employee)
之前添加下面的代码:
1 |
|
这里主要做了2件事:
- 在
DateFormatter
的扩展中添加了格式化器
,它的格式化形式满足JSON中日期的格式,并且是可以重用的。 - 设置
dateEncodingStrategy
和dateDecodingStrategy
为.formatted(.dateFormatter)
,这样编解码时就会使用它去处理日期
运行并检查dateString
的内容,你会发现它符合预期。
处理子类
现在,JSON格式变成了下面这样:
1 |
|
这里将Employee
所需信息分开了。我们打算使用BasicEmployee
去解析employee
。打开本节代码Subclasses
,使BasicEmployee
遵循Codable
:
1 |
|
不出意外,编译器报错了,因为GiftEmployee
并没有遵循Codable
。我们继续添加下面的代码,就可以修正错误了:
1 |
|
这里做了3件事:
- 在
GiftEmployee
中添加了CodingKeys
。和JSON
中的字段名对应。 - 从
decoder
解码出子类的属性值。 - 创建用于解码父类属性的
Decoder
,然后调用父类的方法初始化父类属性。
下面我们继续完成GiftEmployee
的编码方法:
1 |
|
和解码过程类似,我们先编码了子类的属性,然后获取用于编码父类的encoder
。下面测试下结果:
1 |
|
运行并检查giftString
,你会发现其内容符合预期。学习了本节,你就可以处理更复杂的继承数据模型了。
处理混合类型的数组
现在,JSON格式变成了下面这样:
1 |
|
这是个JSON数组,但是其内部元素格式并不一致。打开本节代码Polymorphic types
,可以看到这里使用枚举定义了不同类型的数据。
首先,我们让AnyEmployee
遵循Encodable
协议:
1 |
|
继续在AnyEmployee
中添加下面的代码:
1 |
|
这里我们主要做了两件事:
- 定义了所有可能的键。
- 根据不同类型,对数据进行编码。
在代码的最后添加下面的内容来进行测试:
1 |
|
接下来的编码过程有点复杂。继续添加下面的代码:
1 |
|
解释下上面的代码:
- 获取
KeydContainer
,并获取其所有键。 - 根据不同的键,实行不同的解析策略
- 从
employeesData
中解码出[AnyEmployee]
个人感觉若数组中的元素可以用同一模型来表示,只是字段可能为空时,直接将模型字段设为可选。当然这里也提供了解析不同模型的思路。
处理数组
现在,我们有如下格式JSON:
1 |
|
这里是一个数组,并且其大小写各不相同。此时我们不需要任何CodingKey
,只需使用unkeyed container
。
打开本节代码Unkeyed containers
,添加下面的代码到Label
结构体中:
1 |
|
UnkeyedEncodingContainer
和之前用到的KeyedEncodingContainer
相似,但是它不需要CodingKey
,因为它将编码数据写入JSON数组中。这里我们编码了3中不同的字符串到其中。
继续解码:
1 |
|
这里主要是获取decoder.unkeyedContainer
,获取容器中最后一个值来初始化name
。
处理嵌套在对象中的数组
现在我们有如下格式JSON:
1 |
|
这次,标签对应在了label
字段下。我们需要使用nested unkeyed containers
去进行编解码。
打开本节代码Nested unkeyed containers
,在Toy
中添加下面的代码:
1 |
|
这里我们创建了一个nested unkeyed container
,并填充了3个字符串。运行代码,并查看string
的值,可以看到预期结果。
继续添加下面的代码进行解码:
1 |
|
这里,我们像之前一样,使用unkeyed container
的最后一个值初始化label
字段,只不过获取的是嵌套的容器。
处理可选字段
最后,我们的模型中的属性也可以是可选类型,container
也提供了对应的编解码方法:
1 |
|
总结
今天我们由浅入深的学习了如何在Swift
中处理JSON
。其中自定义Coding keys
、处理子类
等部分需要重点理解。希望对大家有所帮助。