Swift基础知识相关(二) —— 编码和解码(一)

版本记录

版本号 时间
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

ToyEmployee添加Codable遵守:

struct Toy: Codable {
  ...
}
struct Employee: Codable {
  ...
}

Codable本身不是协议,而是另外两个协议的别名:EncodableDecodable。 正如您可能猜到的那样,这两个协议声明类型可以编码为不同的格式并从其中解码。

您不需要再做任何事情,因为ToyEmployee的所有存储属性(stored properties)都是可编码(codable)的。 默认情况下,Swift标准库和基础类型(Swift Standard Library and Foundation )中的许多基本类型(例如,StringURL)都是可编码的。

注意:您可以将可编码类型编码为各种格式,例如Property Lists (PLists)XMLJSON,但是对于本教程,您只能使用JSON。

添加JSONEncoderJSONDecoder来处理toysemployeesJSON编码和解码:

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
}

JSONfavoriteToy中嵌套name,所有JSON键与EmployeeToy存储属性相同,因此您可以根据数据类型层次结构轻松理解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按钮。 要查看结果,可以将值打印print到调试器控制台,或单击结果侧栏中的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 caselooksLikeThis)转换到snake caselooks_like_this_instead)以格式化其JSON的键。

但是EmployeeToy的所有存储属性都只使用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。 你再次保存了(员工的出生日!)

Swift基础知识相关(二) —— 编码和解码(一)_第1张图片

Working With Custom JSON Keys

礼品部门再次更改其API以使用与您的EmployeeToy存储属性不同的JSON key

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "name" : "Teddy Bear"
  }
}

现在,APIgift取代了favoriteToy

这意味着JSON中的字段名称将不再与您的类型中的属性名称匹配。 您可以定义自定义编码键(custom coding keys )以提供属性的编码名称。 您可以通过向类型添加特殊枚举来完成此操作。 打开custom coding keys并在Employee类型中添加此代码:

enum CodingKeys: String, CodingKey {
  case name, id, favoriteToy = "gift"
}

CodingKeys是上面提到的特殊枚举。 它符合CodingKey并具有String原始值。 这里是您将favoriteToy映射到gift的地方。

如果此枚举存在,则只有此处出现的情况将用于编码和解码,因此即使您的属性不需要映射,它也必须包含在枚举中,如nameid在此处所示。

运行playground并查看编码的字符串值 - 您将看到正在使用的新字段名称。 由于自定义编码密钥custom coding keys,JSON不再依赖于您存储的属性。

是时候进行下一次挑战!


Working With Flat JSON Hierarchies

现在,Gifts部门的API不希望其JSON中有任何嵌套类型,因此它们的代码如下所示:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : "Teddy Bear"
}

这与您的模型结构不匹配,因此您需要编写自己的编码逻辑并描述如何编码每个EmployeeToy存储的属性。

首先,打开Keyed containers。 您将看到一个声明为EncodableEmployee类型。 它也在扩展中声明为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) 将nameid属性直接编码到容器中。
  • 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"
    }
  }
}

你在toygift里面的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标准,这对于每个与之合作过的程序员而言都是如此。 JSONEncoderJSONDecoder默认使用日期的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) 将dateEncodingStrategydateDecodingStrategy设置为.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基础知识相关(二) —— 编码和解码(一)_第2张图片
Encoding and decoding in Swift like a pro!

后记

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

Swift基础知识相关(二) —— 编码和解码(一)_第3张图片

你可能感兴趣的:(Swift基础知识相关(二) —— 编码和解码(一))