版本记录
版本号 | 时间 |
---|---|
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([.name, .id])
let customKeys = Set([.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编码和解码,感兴趣的给个赞或者关注~~~