版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2019.07.22 星期一 |
前言
这个专题我们就一起看一下Swfit相关的基础知识。感兴趣的可以看上面几篇。
1. Swift基础知识相关(一) —— 泛型(一)
开始
首先看下主要内容
主要内容:在本教程中,您将学习Swift中的所有编码和解码,探索自定义日期和自定义编码等基础知识和高级主题。
然后看些写作环境
Swift 5, iOS 12, Xcode 10
iOS应用程序的一项常见任务是保存数据并通过网络发送数据。 但在此之前,您需要通过称为编码或序列化(encoding or serialization)的过程将数据转换为合适的格式。

在应用中使用之前,您还需要将通过网络发送的已保存数据转换为合适的格式。 该反向过程称为解码或反序列化(decoding or deserialization)。

在本教程中,您将通过管理自己的toy store了解有关Swift编码和解码的所有信息。 您将在此过程中探索以下主题:
- 在
snake case and camel case之间切换。 - 定义自定义编码key。
- 使用
keyed, unkeyed and nested containers。 - 处理嵌套类型,日期,子类和多态类型。
有很多东西可以了解,所以是时候开始了!
注意:本教程假定您具有
JSON的基本知识。 如果您需要快速浏览,请查看此 cheat sheet。
打开起始项目,通过转到View ▸ Navigators ▸ Show Project Navigator,确保在Xcode中可以看到Project navigator。 打开Nested types。
为Toy和Employee添加Codable遵守:
struct Toy: Codable {
...
}
struct Employee: Codable {
...
}
Codable本身不是协议,而是另外两个协议的别名:Encodable和Decodable。 正如您可能猜到的那样,这两个协议声明类型可以编码为不同的格式并从其中解码。
您不需要再做任何事情,因为Toy和Employee的所有存储属性(stored properties)都是可编码(codable)的。 默认情况下,Swift标准库和基础类型(Swift Standard Library and Foundation )中的许多基本类型(例如,String和URL)都是可编码的。
注意:您可以将可编码类型编码为各种格式,例如
Property Lists (PLists),XML或JSON,但是对于本教程,您只能使用JSON。
添加JSONEncoder和JSONDecoder来处理toys和employees的JSON编码和解码:
let encoder = JSONEncoder()
let decoder = JSONDecoder()
这就是使用JSON所需的全部内容。 第一次编码和解码挑战的时间!
Encoding and Decoding Nested Types
Employee包含一个Toy属性 - 它是一个嵌套类型(nested type)。 编码employee的JSON结构与Employee结构匹配:
{
"name" : "John Appleseed",
"id" : 7,
"favoriteToy" : {
"name" : "Teddy Bear"
}
}
public struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy
}
JSON在favoriteToy中嵌套name,所有JSON键与Employee和Toy存储属性相同,因此您可以根据数据类型层次结构轻松理解JSON结构。 如果您的属性名称与您的JSON字段名称匹配,并且您的属性都是Codable,那么您可以非常轻松地转换为JSON或从JSON转换。 你现在就试试。
Gifts部门为员工提供他们喜欢的玩具作为生日礼物。 添加以下代码以将员工的数据发送到礼品部门:
// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!
以下是此代码的工作原理:
- 1) 使用
encode(_:)将employee编码为JSON(我告诉过你这很简单!)。 - 2) 从编码
data创建一个字符串以使其可视化。
注意:按
Shift-Return可将playground运行到当前行,或单击蓝色play按钮。 要查看结果,可以将值打印Show Result按钮。
编码过程生成有效数据,因此礼品部门可以重新创建员工:
let sameEmployee = try decoder.decode(Employee.self, from: data)
在这里,您已经使用decode(_:from :)将data解码回Employee ......您已经让您的员工非常开心。 按蓝色play按钮以运行Playground并查看结果。
是时候进行下一次挑战!
Switching Between Snake Case and Camel Case Formats
礼品部门API已经从camel case(looksLikeThis)转换到snake case(looks_like_this_instead)以格式化其JSON的键。
但是Employee和Toy的所有存储属性都只使用camel case的情况! 幸运的是,Foundation为您提供服务。
打开Snake case vs camel case并在创建编码器和解码器之后添加以下代码,然后再使用它们:
encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
在这里,您将keyEncodingStrategy设置为.convertToSnakeCase以对employee进行编码。 您还将keyDecodingStrategy设置为.convertFromSnakeCase以解码snakeData。
运行playground并检查snakeString。 在这种情况下,编码的employee看起来像这样(双关语):
{
"name" : "John Appleseed",
"id" : 7,
"favorite_toy" : {
"name" : "Teddy Bear"
}
}
JSON中的格式现在是favorite_toy,并且您已将其转换回Employee结构中的favoriteToy。 你再次保存了(员工的出生日!)

Working With Custom JSON Keys
礼品部门再次更改其API以使用与您的Employee和Toy存储属性不同的JSON key:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"name" : "Teddy Bear"
}
}
现在,API用gift取代了favoriteToy。
这意味着JSON中的字段名称将不再与您的类型中的属性名称匹配。 您可以定义自定义编码键(custom coding keys )以提供属性的编码名称。 您可以通过向类型添加特殊枚举来完成此操作。 打开custom coding keys并在Employee类型中添加此代码:
enum CodingKeys: String, CodingKey {
case name, id, favoriteToy = "gift"
}
CodingKeys是上面提到的特殊枚举。 它符合CodingKey并具有String原始值。 这里是您将favoriteToy映射到gift的地方。
如果此枚举存在,则只有此处出现的情况将用于编码和解码,因此即使您的属性不需要映射,它也必须包含在枚举中,如name和id在此处所示。
运行playground并查看编码的字符串值 - 您将看到正在使用的新字段名称。 由于自定义编码密钥custom coding keys,JSON不再依赖于您存储的属性。
是时候进行下一次挑战!
Working With Flat JSON Hierarchies
现在,Gifts部门的API不希望其JSON中有任何嵌套类型,因此它们的代码如下所示:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
这与您的模型结构不匹配,因此您需要编写自己的编码逻辑并描述如何编码每个Employee和Toy存储的属性。
首先,打开Keyed containers。 您将看到一个声明为Encodable的Employee类型。 它也在扩展中声明为Decodable。 这种拆分是为了保持你使用Swift结构体获得的free member-wise初始化程序。 如果在主定义中声明了init方法,则会丢失该方法。 在Employee中添加此代码:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
func encode(to encoder: Encoder) throws {
// 2
var container = encoder.container(keyedBy: CodingKeys.self)
// 3
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
try container.encode(toy.name, forKey: .gift)
}
对于您在上面看到的简单情况,编译器会自动为您实现encode(to :)。 现在,你自己做了。 这是代码正在做的事情:
- 1) 创建一组编码键来表示您的
JSON字段。 因为您没有进行任何映射,所以您不需要将它们声明为字符串,因为没有原始值。 - 2) 创建
KeyedEncodingContainer。 这就像您可以在编码时存储属性的字典。 - 3) 将
name和id属性直接编码到容器中。 - 4) 使用礼品密钥将
toy的名称直接编码到容器中
运行playground并检查编码字符串的值 - 它将匹配本节顶部的JSON。 能够选择对哪些键进行编码的属性为您提供了很大的灵活性。
解码过程与编码过程相反。 用这个替换可怕的fatalError("To do"):
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)
与编码一样,对于简单的情况,编译器会自动为您生成init(from :),但是您自己就是这样做的。 这是代码正在做的事情:
- 1) 从解码器获取一个键控容器,它将包含JSON中的所有属性。
- 2) 使用适当的类型和编码key从容器中提取
name and id。 - 3) 提取礼物的名称,并使用它来构建
Toy并将其分配给正确的属性。
添加一行以从平面JSON重新创建employee:
let sameEmployee = try decoder.decode(Employee.self, from: data)
这一次,您选择了哪些属性来解码哪些键,并有机会在解码过程中进一步工作。 手动编码和解码功能强大,为您提供灵活性。 您将在接下来的挑战中了解更多相关信息。
Working With Deep JSON Hierarchies
礼品部门希望确保员工的生日礼物只能是玩具,因此其API会生成如下所示的JSON:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
你在toy和gift里面的toy同时嵌入name。 与Employee层次结构相比,JSON结构添加了额外级别的缩进,因此在这种情况下您需要使用嵌套的键控容器(nested keyed containers)作为礼物。
打开嵌套的键控容器并将以下代码添加到Employee:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
// 2
enum GiftKeys: CodingKey {
case toy
}
// 3
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
var giftContainer = container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
try giftContainer.encode(toy, forKey: .toy)
}
这就是上面代码的工作原理:
- 1) 创建
top-level coding keys。 - 2) 创建另一组编码键,您将使用它来创建另一个容器。
- 3) 按照您习惯的方式对
name and id进行编码。 - 4) 创建一个嵌套容器
nestedContainer(keyedBy:forKey :)并用它编码toy。
运行playground并检查编码的字符串以查看多级JSON。 您可以使用尽可能多的嵌套容器,因为您的JSON具有缩进级别。 在现实世界的API中使用复杂而深入的JSON层次结构时,这很方便。
在这种情况下,解码很简单。 添加以下扩展名:
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
let giftContainer = try container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
}
}
let sameEmployee = try decoder.decode(Employee.self, from: nestedData)
您已使用嵌套解码容器(nested decoding container)将nestedData解码为Employee。
Encoding and Decoding Dates
礼品部门需要知道员工的生日才能发送礼物,因此他们的JSON看起来像这样:
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : "29-05-2019",
"toy" : {
"name" : "Teddy Bear"
}
}
日期没有JSON标准,这对于每个与之合作过的程序员而言都是如此。 JSONEncoder和JSONDecoder默认使用日期的timeIntervalSinceReferenceDate的双重表示,这在并不常见。
您需要使用日期策略(date strategy)。 在try encoder.encode(employee)语句之前,将此代码块添加到日期(Dates):
// 1
extension DateFormatter {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
return formatter
}()
}
// 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
这是代码的作用:
- 1) 创建与所需格式匹配的日期格式化程序。 它作为
DateFormatter的静态属性添加,因为这是您的代码的良好实践,因此格式化程序是可重用的。 - 2) 将
dateEncodingStrategy和dateDecodingStrategy设置为.formatted(.dateFormatter),告诉编码器和解码器在编码和解码日期时使用格式化程序。
检查dateString并检查日期格式是否正确。 您已确保礼品部门将按时交付礼品 - 即将推出!
还有一些挑战,你已经完成了。
Encoding and Decoding Subclasses
Gifts部门API可以根据类层次结构(class hierarchies)处理JSON:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
employee匹配没有toy or birthday的基类结构。 打开Subclasses并使BasicEmployee符合Codable:
class BasicEmployee: Codable {
这会给你一个错误,因为GiftEmployee还不是Codable。 通过向GiftEmployee添加以下内容来纠正:
// 1
enum CodingKeys: CodingKey {
case employee, birthday, toy
}
// 2
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
birthday = try container.decode(Date.self, forKey: .birthday)
toy = try container.decode(Toy.self, forKey: .toy)
// 3
let baseDecoder = try container.superDecoder(forKey: .employee)
try super.init(from: baseDecoder)
}
此代码涵盖解码:
- 1) 添加相关的编码密钥。
- 2) 解码特定于子类的属性。
- 3) 使用
superDecoder(forKey :)获取一个适合传递给超类的init(from :)方法的解码器实例,然后初始化超类。
现在在GiftEmployee中实现编码:
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
let baseEncoder = container.superEncoder(forKey: .employee)
try super.encode(to: baseEncoder)
}
它是相同的模式,但您使用superEncoder(forKey :)为超类准备编码器。 将以下代码添加到playground的末尾以测试您的可编码子类:
let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(),
toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)
检查giftString的值以查看您的工作! 您可以在应用程序中处理更复杂的类层次结构。 是时候进行下一次挑战!
Handling Arrays With Mixed Types
Gifts部门API公开了适用于不同类型员工的JSON:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
此JSON数组是多态的,因为它包含默认和自定义employees。 打开Polymorphic types,您将看到不同类型的员工由枚举表示。 首先,声明枚举是Encodable:
enum AnyEmployee: Encodable {
然后将此代码添加到枚举的主体:
// 1
enum CodingKeys: CodingKey {
case name, id, birthday, toy
}
// 2
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .defaultEmployee(let name, let id):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
case .customEmployee(let name, let id, let birthday, let toy):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
case .noEmployee:
let context = EncodingError.Context(codingPath: encoder.codingPath,
debugDescription: "Invalid employee!")
throw EncodingError.invalidValue(self, context)
}
}
以下是此代码的用途:
- 1) 定义足够的编码密钥以涵盖所有可能的情况。
- 2) 对有效员工进行编码,并对无效员工抛出
EncodingError.invalidValue(_:_ :)。
通过将以下内容添加到playground的末尾来测试您的编码:
let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7),
AnyEmployee.customEmployee("John Appleseed", 7, Date(), toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!
检查employeesString的值以查看混合数组。
解码有点复杂,因为在决定如何继续之前,你必须弄清楚JSON中的内容。 将以下代码添加到playground:
extension AnyEmployee: Decodable {
init(from decoder: Decoder) throws {
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerKeys = Set(container.allKeys)
let defaultKeys = Set<CodingKeys>([.name, .id])
let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
// 2
switch containerKeys {
case defaultKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
self = .defaultEmployee(name, id)
case customKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
let birthday = try container.decode(Date.self, forKey: .birthday)
let toy = try container.decode(Toy.self, forKey: .toy)
self = .customEmployee(name, id, birthday, toy)
default:
self = .noEmployee
}
}
}
// 4
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
这就是它的工作原理:
- 1) 像往常一样获取一个键控容器,然后检查
allKeys属性以确定JSON中存在哪些键。 - 2) 检查
containerKeys是否与默认员工或自定义员工所需的密钥匹配,并提取相关属性;否则,建立一个.noEmployee。 如果没有合适的默认值,您可以选择在此处抛出错误。 - 3) 将
employeesData解码为[AnyEmployee]。
您可以根据具体类型对employeesData中的每个employee进行解码,就像编码一样。
只留下两个挑战 - 下一个挑战!
Working With Arrays
礼品部门为员工的生日礼物添加标签;他们的JSON看起来像这样:
[
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
JSON数组包含小写,大写和常规标签名称。 这次你不需要任何密钥,所以你使用一个unkeyed container。
打开Unkeyed container容器并将编码代码添加到Label:
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(toy.name.lowercased())
try container.encode(toy.name.uppercased())
try container.encode(toy.name)
}
UnkeyedEncodingContainer就像你目前使用的容器一样工作,除了......你猜对了,没有键。 可以将其视为写入JSON数组而不是JSON字典。 您将三个不同的字符串编码到容器中。
运行playground并检查labelString以查看您的数组。
这是解码的外观。 将以下代码添加到playground的末尾:
extension Label: Decodable {
// 1
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var name = ""
while !container.isAtEnd {
name = try container.decode(String.self)
}
toy = Toy(name: name)
}
}
// 2
let sameLabel = try decoder.decode(Label.self, from: labelData)
这就是上面代码的工作原理:
- 1) 获取解码器的无键解码容器
(unkeyed decoding container),并使用decode(_ :)循环解码,以解码最终的,格式正确的标签名称。 - 2) 使用未加密码的解码容器
(unkeyed decoding container)将labelData解码为Label。
您遍历整个解码容器,因为最后会出现正确的标签名称。
你最后一次挑战的时间!
Working With Arrays Within Objects
礼品部门想要查看员工生日礼物的名称和标签,因此其API生成的JSON如下所示:
{
"name" : "Teddy Bear",
"label" : [
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
}
您将标签名称嵌套在label内。 与前一个挑战相比,JSON结构增加了额外的缩进级别,因此在这种情况下,您需要使用嵌套的无键容器(nested unkeyed containers)作为label。
打开Nested unkeyed containers并将以下代码添加到Toy:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var labelContainer = container.nestedUnkeyedContainer(forKey: .label)
try labelContainer.encode(name.lowercased())
try labelContainer.encode(name.uppercased())
try labelContainer.encode(name)
}
在这里,您将创建一个嵌套的无键容器,并使用三个标签值填充它。 运行playground并检查string以检查结构是否正确。
如果JSON具有更多缩进级别,则可以使用更多嵌套容器。 将解码代码添加到playground页面:
extension Toy: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
var labelName = ""
while !labelContainer.isAtEnd {
labelName = try labelContainer.decode(String.self)
}
label = labelName
}
}
let sameToy = try decoder.decode(Toy.self, from: data)
这遵循与以前相同的模式,通过数组并使用最终值来设置label的值,但是来自嵌套的未键控容器(nested unkeyed container)。
恭喜您完成所有挑战!

后记
本篇主要讲述了Swift编码和解码,感兴趣的给个赞或者关注~~~
