春天前销销售 - 保存一切。所有视频。所有书籍。 现在50%的折扣。

建立您的移动发展技能并保存!通过终极书籍和视频订阅,继续前进。作为春季前销销售的一部分,仅为149美元/年。

首页 iOS.& Swift Tutorials

在Swift中编码和解码

在本教程中,您将了解Swift中的所有编码和解码,探索自定义日期和自定义编码等基础和高级主题。

4.5/5 41评级

版本

  • Swift 5,iOS 12,Xcode 10
更新注释:Cosminpupăză更新了Xcode 11的本教程,iOS 13和Swift 5.1 Eli Ganem写了原创。

iOS应用程序的共同任务是保存数据并通过网络发送。但在您可以做到之前,您需要通过调用的过程将数据转换为合适的格式 编码 或者 序列化.

被编码的数据示例。

在应用程序中使用之前,您还需要将通过网络发送的保存数据转换为合适的格式。调用此反向过程 解码 或者 反序列化.

被解码的数据示例。

在本教程中,通过管理您自己的玩具商店,您将了解所有您需要了解迅速的编码和解码。您将沿途浏览以下主题:

  • 在蛇壳和骆驼壳格式之间切换。
  • 定义自定义编码键。
  • 使用钥匙,不合盖和嵌套容器。
  • 处理嵌套类型,日期,子类和多态类型。

覆盖很多,所以现在是时候开始了! :]

笔记:本教程假设您有一个基本的知识 杰森。看看这一点 骗子 如果您需要快速概述。

入门

使用初学游乐场 下载材料 链接在教程的顶部或底部。

确保这一点 项目导航员 通过Xcode可见 查看▸导航器▸显示Project Navigator。打开 嵌套类型.

Add Codable conformance to Toy and Employee:

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

Codable isn’t a protocol on it’s own, but an alias for two other protocols: Encodable and Decodable. As you might guess, those two protocols declare that types can be encoded to and decoded from a different format.

你不需要做更多,因为所有的 存储属性 of both Toy and Employee are codable. Many basic types in the Swift Standard Library and Foundation types (for example, String and URL) are codable by default.

笔记:您可以将可编码类型编码为各种格式,例如 财产清单(Plists), XML. 或者 杰森,但对于本教程,您只能与JSON合作。

Add a 杰森Encoder and a 杰森Decoder to handle 杰森 编码 and decoding of toys and employees:

let encoder = JSONEncoder()
let decoder = JSONDecoder()

这就是json所需的一切。您的第一个编码和解码挑战的时间!

编码和解码嵌套类型

Employee contains a Toy property — it’s a 嵌套类型. The JSON structure of your encoded employee matches the Employee struct:

{
  "姓名" : "John Appleseed",
  "id" : 7,
  "favoriteToy" : {
    "姓名" : "Teddy Bear"
  }
}
public struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy
}

The JSON nests 姓名 inside favoriteToy and all the JSON keys are the same as the Employee and Toy stored properties, so you can easily understand the JSON structure based on your data types hierarchy. If your property names match your JSON field names, and your properties are all Codable, then you can convert to or from JSON very easily. You’ll try that now.

礼品部门为员工提供了他们最喜欢的玩具作为生日礼物。添加以下代码以将您的员工的数据发送给礼品部门:

// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!

以下是此代码的工作原理:

  1. Encode employee to JSON with encode(_:) (I told you it was easy!).
  2. Create a string from the encoded data to visualize it.
笔记: 按 转移回归 to run the playground up to your current line, or click the blue play button. To see results, you can print values to the debugger console or click the 显示结果 按钮在结果侧边栏中。

编码过程生成有效数据,因此礼品部门可以重新创建员工:

let sameEmployee = try decoder.decode(Employee.self, from: data)

Here, you’ve used decode(_:from:) to decode data back to Employee… and you’ve made your employee very happy. Press the blue play button to run the Playground and see the results.

你的下一个挑战的时间!

在蛇壳和骆驼壳格式之间切换

礼物部门API已切换 骆驼香烟盒 (which looksLikeThis) to 蛇案 (which looks_like_this_instead) to format keys for its JSON.

But all of the stored properties of Employee and Toy use camel case only! Fortunately, Foundation has you covered.

打开 蛇盒与骆驼盒 并在创建编码器和解码器之后添加以下代码,然后在它们使用之前:

encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase

Here, you set keyEncodingStrategy to .convertToSnakeCase to encode employee. You also set keyDecodingStrategy to .convertFromSnakeCase to decode snakeData.

Run the playground and inspect snakeString. The encoded employee looks like this in this case (pun intended):

{
  "姓名" : "John Appleseed",
  "id" : 7,
  "favorite_toy" : {
    "姓名" : "Teddy Bear"
  }
} 

The formatting in JSON is now favorite_toy and you’ve transformed it back to favoriteToy in the Employee struct. You’ve saved the (employee’s birth)day again! :]

狗宣布的图像:生日快乐!

使用自定义JSON键

The Gifts department has changed its API again to use different JSON keys than your Employee and Toy 存储属性 use:

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

Now, the API replaces favoriteToy with gift.

这意味着JSON中的字段名称将不再与类型中的属性名称匹配。你可以定义 自定义编码键 为您的属性提供编码名称。您通过向您的类型添加特殊枚举来执行此操作。打开 自定义编码键 and add this code inside the Employee type:

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

CodingKeys is the special enum mentioned above. It conforms to CodingKey and has String raw values. Here’s where you map favoriteToy to gift.

If this enum exists, only the cases present here will be used for encoding and decoding, so even if your property doesn’t require mapping, it has to be included in the enum, as 姓名 and id are here.

运行游乐场并查看编码的字符串值 - 您将看到使用的新字段名称。由于自定义编码键,JSON不依赖于存储的属性。

你的下一个挑战的时间!

使用平的JSON层次结构

现在礼物部门的API不想要任何 嵌套类型 在其json中,所以他们的代码如下所示:

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

This doesn’t match your model structure, so you need to write your own encoding logic and describe how to encode each Employee and Toy stored property.

开始,打开 钥匙容器. You’ll see an Employee type which is declared as Encodable. It’s also declared Decodable in an extension. This split is to keep the free member-wise initializer you get with Swift structs. If you declare an init method in the main definition, you lose that. Add this code inside 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(favoriteToy.name, forKey: .gift)
}

For simple cases like you’ve seen above, encode(to:) is automatically implemented for you by the compiler. Now, you’re doing it yourself. Here’s what the code is doing:

  1. 创建一组编码键以表示您的JSON字段。因为你没有做任何映射,所以你不需要将它们声明为字符串,因为没有原始值。
  2. Create a KeyedEncodingContainer. This is like a dictionary you can store your properties in as you encode them.
  3. Encode the 姓名 and id properties directly to the container.
  4. Encode the toy’s name directly into the container, using the gift key.

运行游乐场并检查编码字符串的值 - 它将与此部分顶部的JSON匹配。能够选择哪个属性来编码密钥为您提供了很多灵活性。

The decoding process is the opposite of the encoding process. Replace the scary fatalError("To do") with this:

// 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)

As with encoding, for simple cases init(from:) is made automatically for you by the compiler, but here you’re doing it yourself. Here’s what the code is doing:

  1. 从解码器获取键控的容器,这将包含JSON中的所有属性。
  2. Extract the 姓名 and id values from the container using the appropriate type and coding key.
  3. Extract the gift’s name, and use that to build a Toy and assign it to the correct property.

添加一条线以从平板JSON重新创建员工:

let sameEmployee = try decoder.decode(Employee.self, from: data)

这次,您已选择哪些属性来解码哪个键并有机会在解码期间进行进一步的工作。手动编码和解码功能强大,并为您提供灵活性。您将在下一个挑战中了解更多信息。

使用深json层次结构

礼品部门希望确保员工的生日礼物只能是玩具,所以它的API会生成JSON,看起来像这样:

{
  "姓名" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "toy" : {
      "姓名" : "Teddy Bear"
    }
  }
}

You nest 姓名 inside toy and toy inside gift. The JSON structure adds an extra level of indentation compared to the Employee hierarchy, so you need to use 嵌套键控容器 for gift in this case.

打开 嵌套键控容器 and add the following code to 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(favoriteToy, forKey: .toy)
}

这就是上述代码的工作原理:

  1. 创建顶级编码键。
  2. 创建另一组编码键,您将使用它来创建另一个容器。
  3. Encode the 姓名 and id the way you’re used to.
  4. Create a nested container nestedContainer(keyedBy:forKey:) and encode favoriteToy with it.

运行游乐场并检查编码的字符串以查看您的多级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)

You’ve decoded nestedData to Employee using a 嵌套解码容器.

编码和解码日期

礼物部门需要了解员工的生日,以发出礼物,所以他们的JSON看起来像这样:

{
  "id" : 7,
  "姓名" : "John Appleseed",
  "birthday" : "29-05-2019",
  "toy" : {
    "姓名" : "Teddy Bear"
  }
}

There is no JSON standard for dates, much to the distress of every programmer who’s ever worked with them. 杰森Encoder and 杰森Decoder will by default use a double representation of the date’s timeIntervalSinceReferenceDate, which is not very common in the wild.

你需要使用一个 日期战略。将此代码块添加到 日期, before the try encoder.encode(employee) statement:

// 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. Create a date formatter matching your desired format. It’s added as a static property on DateFormatter as this is good practice for your code, so formatters are reusable.
  2. Set dateEncodingStrategy and dateDecodingStrategy to .formatted(.dateFormatter) to tell the encoder and decoder to use the formatter while encoding and decoding dates

Inspect the dateString and check the date format is correct. You’ve made sure the Gifts department will deliver the gifts on time — way to go! :]

只是更多的挑战,你已经完成了。

编码和解码子类

礼品部门API可以根据的基于JSON处理 班级层次结构:

{
  "toy" : {
    "姓名" : "Teddy Bear"
  },
  "employee" : {
    "姓名" : "John Appleseed",
    "id" : 7
  },
  "birthday" : 580794178.33482599
}

employee 匹配 基类 structure which has no toy 或者 birthday。打开 亚级 and make BasicEmployee conform to Codable:

class BasicEmployee: Codable {

This will give you an error, because GiftEmployee is not Codable yet. Correct that by adding the following to 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. Use superDecoder(forKey:) to get a decoder instance suitable to pass to the init(from:) method of the superclass, then initialize the superclass.

Now implement encoding in 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)
}

It’s the same pattern, but you use superEncoder(forKey:) to prepare the encoder for the superclass. Add the following code to the end of the playground to test out your codable subclass:

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)

Inspect the value of giftString to see your work in action! You can handle even more complex class hierarchies in your apps. 你的下一个挑战的时间!

用混合类型处理阵列

礼品部门API公开了与不同类型的员工合作的JSON:

[
  {
    "姓名" : "John Appleseed",
    "id" : 7
  },
  {
    "id" : 7,
    "姓名" : "John Appleseed",
    "birthday" : 580797832.94787002,
    "toy" : {
      "姓名" : "Teddy Bear"
    }
  }
]

此JSON数组是多态,因为它包含默认和自定义员工。打开 多态性类型 and you’ll see that the different types of employee are represented by an enum. First, declare that the enum is 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. Encode valid employees and throw EncodingError.invalidValue(_:_:) for invalid ones.

通过将以下内容添加到游乐场的结尾来测试编码:

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)!

Inspect the value of employeesString to see your mixed array.

笔记:想要了解更多关于SWIFT的多态性吗?查看面向对象的编程教程: Swift的面向对象的编程.

解码有点复杂,因为在您可以决定如何继续之前,您必须锻炼JSON中的内容。将以下代码添加到游乐场:

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. Get a keyed container as usual, then inspect the allKeys property to determine which keys were present in the JSON.
  2. Check whether the containerKeys 匹配 keys needed for a default employee or a custom employee and extract the relevant properties; otherwise, make a .noEmployee. You could choose to throw an error here if there was no suitable default.
  3. Decode employeesData to [AnyEmployee].

You decode each employee in employeesData based on its concrete type, just as you do for encoding.

下一个挑战留下了两个挑战!

使用阵列

礼品部门为员工生日礼物添加标签;他们的json看起来像这样:

[
  "teddy bear",
  "TEDDY BEAR",
  "Teddy Bear"
]

杰森数组包含小写,大写和常规标签名称。这次你不需要任何键,所以你使用一个 不合格的容器.

打开 不合格的容器 and add the encoding code to 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)
}

An UnkeyedEncodingContainer works just like the containers you’ve been using so far except… you guessed it, there are no keys. Think of it as writing to JSON arrays instead of JSON dictionaries. You encode three different strings to the container.

Run the playground and inspect labelString to see your array.

这是解码看起来的看法。将以下代码添加到游乐场的末尾:

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. 获得解码器 不合格解码容器 and loop through it with decode(_:) to decode the final, correctly-formatted label name.
  2. Decode labelData to Label using your 不合格解码容器.

由于正确的标签名称到底,因此您循环通过整个解码容器。

时间为您的最后一个挑战!

使用对象中的数组

礼品部门希望看到员工生日礼物的名称和标签,因此它的API会产生如下所示的JSON:

{
  "姓名" : "Teddy Bear",
  "label" : [
    "teddy bear",
    "TEDDY BEAR",
    "Teddy Bear"
  ]
}

You nest the label names inside label. The JSON structure adds an extra level of indentation compared to the previous challenge, so you need to use 嵌套不合格的容器 for label in this case.

打开 嵌套不合格的容器 and add the following code to 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)
}

Here you are creating a nested unkeyed container and filling it with the three label values. Run the playground and inspect string to check the structure is right.

如果您的JSON具有更多的缩进级别,您可以使用更多嵌套容器。将解码代码添加到游乐场页面:

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)

This follows the same pattern as before, working through the array and using the final value to set the value of label, but from a nested unkeyed container.

祝贺完成所有挑战! :]

像Pro威瑞斯的编码和解码!

像Pro威瑞斯的编码和解码!

然后去哪儿?

使用的最后一个游乐场 下载材料 按钮在教程的顶部或底部。

如果您想了解有关SWIFT中的编码和解码的更多信息,请查看我们的 在iOS中保存数据 video course. It covers 杰森, Property Lists, XML. and much more!

我希望您享受本教程,如果您有任何疑问或意见,请加入下面的论坛讨论! :]

平均评级

4.5/5

为此内容添加评级

41 ratings

更像这样的

贡献者

注释