美文网首页
Swift&JSON 从入门到精通

Swift&JSON 从入门到精通

作者: 大菠萝_DABLO | 来源:发表于2022-01-24 11:24 被阅读0次

    在iOS中最常见的工作是将数据保存起来并通过网络传输。但是在这之前,你需要将数据通过编码序列化转换成合适的格式。

    图片

    同样的,在你使用这些数据之前,你也需要将其转换成合适的格式。这个相反的过程被称为解码反序列化

    图片

    在这个教程中,你将学习到所有使用Swift进行编解码所需要的知识。包括这些:

    1. 蛇形命名驼峰命名格式之间转换
    2. 自定义Coding keys
    3. 使用keyed,unkeyednested容器
    4. 处理嵌套类型,日期类型以及子类

    这确实有点多,是时候开始动手了!

    开始动手

    链接:提取码:15B7

    下载完成后,starter是该教程使用的版本。final是最终完成的版本。

    我们打开本节代码Nested types。使ToyEmployee遵循Codable协议:

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

    Codable本身并不是一个协议,它只是另外两个协议的别名:EncodableDecodable。你也行已经猜到了,这两个协议就是代表那些可以被编解码的类型。

    你无需再做其他事情,因为ToyEmployee的所有存储属性都是Codable的。Swift标准库中大多数类型(例如StringURL)都是支持Codable的。

    添加一个JSONEncoderJSONDecoder来处理toysemployees的编解码:

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

    操作JSON我们只需做这些!下面进入第一个挑战!

    编解码嵌套类型

    Employee包含了一个Toy属性(这是个嵌套类型)。编码后的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数据将name嵌套在favoriteToy之中,并且所有的JSON字段名与ToyEmployee的存储属性名相同,所以基于结构体的类型体系,JSON的结构很容易理解。

    如果属性名称和JSON的字段名都相同,并且属性都是Codable的,那么我们可以很容易的将JSON转换为数据模型,或者反过来。现在来试一试:

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

    这里做了2件事:

    1. employee使用encode(_:)编码成JSON。是不是很简单!
    2. 从上一步的data中创建String,一遍可以查看其内容。

    这里的编码过程会产生合法的数据,所以我们可以使用它重新创建employee

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

    好了,可以开始下一个挑战了!

    蛇形命名驼峰命名格式之间转换

    现在,假设JSON的键名从驼峰格式(这样looksLikeThis)转换成了蛇形格式(这样looks_like_this_instead)。但是,ToyEmployee的存储属性只能使用驼峰格式。幸运的是Foundation考虑到了这种情况。

    打开本节代码Snake case vs camel case,在编解码器创建之后使用之前的位置添加下面的代码:

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

    运行代码,检查snakeString,编码后的employee产生下面的内容:

    {
      "name" : "John Appleseed",
      "id" : 7,
      "favorite_toy" : {
        "name" : "Teddy Bear"
      }
    }
    
    图片

    自定义Coding keys

    现在,假设JOSN的格式再一次改变,其使用的字段名和ToyEmployee中存储属性名不一致了:

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

    可以看到,这里使用gift代替了原来的favoriteToy。这种情况我们需要自定义Coding keys。在我们的类型中添加一个特殊的枚举类型。打开本节代码Custom coding keys,在Employee中添加下面的代码:

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

    这个特殊的枚举遵循了CodingKey协议,并使用String类型的原始值。在这里我们可以让favoriteToygift一一对应起来。

    在编解码过程中,只会操作出现在枚举中的cases,所以即使那些不需要指定一一对应的属性,也需要在枚举中包含,就像这里的nameid

    运行playground,然后查看string的值,你会发现JSON字段名不在依赖存储属性名称,这得益于自定义的Coding keys

    继续下一个挑战!

    处理扁平化的JSON

    现在,JSON的格式变成下面这样:

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

    这里不在有嵌套结构,和我们的模型结构不一致了。这种情况我们需要自定义编解码过程。

    打开本节代码Keyed containers。这里有个Employee类型,它遵循了Encodable。同时我们使用extension让它遵循了Decodable

    这样做的好处是,可以保留结构体的逐一成员构造器。如果我们在定义Employee时让它遵循Decodable,它将失去这个构造器。添加下面的代码到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)
    }
    

    在之前简单(指属性名和键名一一对应且嵌套层级相同)的示例中,encode(to:)方法由编译器自动实现了。现在我们需要手动实现。

    1. 创建CodingKeys表示JSON的字段。因为我们没有做任何的关系映射,所以不必声明它的原始类型为String
    2. encoder中获取KeyedEncodingContainer容器。这就像一个字典,我们可以存储属性的值到其中,这样就进行了编码。
    3. 编码nameid属性到容器中。
    4. 使用gift键,直接将toy的名字编码到容器中。

    运行playground,然后查看string的值,你会发现它符合上面JSON的格式。我们可以选择使用什么字段名编码一个属性值,这给了我们很大的灵活性。

    和编码过程类似,简单版本的init(from:)方法可以由编译器自动实现。但是这里我们需要手动实现,使用下面的代码替换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)
    

    然后添加下面的代码,就可以从JSON中重新创建employee

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

    处理多级嵌套的JSON

    现在,JSON的格式变成下面这样:

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

    name字段在toy字段中,而toy又在gift字段中。如何解析成我们定义的数据模型呢?

    打开本节代码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(favoriteToy, forKey: .toy)
    }
    

    这里做了几件事:

    1. 创建顶层的CodingKeys
    2. 创建用于解析gift字段的CodingKeys,后续使用它创建容器
    3. 使用顶层容器编码nameid
    4. 使用nestedContainer(keyedBy:forKey:)方法获取用于编码gift字段的容器,并将favoriteToy编码进去

    运行并查看string的值,你会发现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)
    

    好了,我们已经搞定了嵌套类型的容器。并从其中解码出了sameEmployee

    处理日期类型

    现在,JSON里添加了日期字段,就像下面这样:

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

    JSON中并没有标准的日期格式。在JSONEncoderJSONDecoder使用日期类的timeIntervalSinceReferenceDate方法去处理(Date(timeIntervalSinceReferenceDate: interval))。

    这里我们需要指定日期转换策略。打开本节代码Dates,在try encoder.encode(employee)之前添加下面的代码:

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

    这里主要做了2件事:

    1. DateFormatter的扩展中添加了格式化器,它的格式化形式满足JSON中日期的格式,并且是可以重用的。
    2. 设置dateEncodingStrategydateDecodingStrategy.formatted(.dateFormatter),这样编解码时就会使用它去处理日期

    运行并检查dateString的内容,你会发现它符合预期。

    处理子类

    现在,JSON格式变成了下面这样:

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

    这里将Employee所需信息分开了。我们打算使用BasicEmployee去解析employee。打开本节代码Subclasses,使BasicEmployee遵循Codable

    class BasicEmployee: Codable {
    

    不出意外,编译器报错了,因为GiftEmployee并没有遵循Codable。我们继续添加下面的代码,就可以修正错误了:

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

    这里做了3件事:

    1. GiftEmployee中添加了CodingKeys。和JSON中的字段名对应。
    2. decoder解码出子类的属性值。
    3. 创建用于解码父类属性的Decoder,然后调用父类的方法初始化父类属性。

    下面我们继续完成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)
    }
    

    和解码过程类似,我们先编码了子类的属性,然后获取用于编码父类的encoder。下面测试下结果:

    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,你会发现其内容符合预期。学习了本节,你就可以处理更复杂的继承数据模型了。

    处理混合类型的数组

    现在,JSON格式变成了下面这样:

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

    这是个JSON数组,但是其内部元素格式并不一致。打开本节代码Polymorphic types,可以看到这里使用枚举定义了不同类型的数据。

    首先,我们让AnyEmployee遵循Encodable协议:

    enum AnyEmployee: Encodable { ... }
    

    继续在AnyEmployee中添加下面的代码:

    // 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. 根据不同类型,对数据进行编码。

    在代码的最后添加下面的内容来进行测试:

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

    接下来的编码过程有点复杂。继续添加下面的代码:

    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
        }
      }
    }
    // 3
    let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
    

    解释下上面的代码:

    1. 获取KeydContainer,并获取其所有键。
    2. 根据不同的键,实行不同的解析策略
    3. employeesData中解码出[AnyEmployee]

    个人感觉若数组中的元素可以用同一模型来表示,只是字段可能为空时,直接将模型字段设为可选。当然这里也提供了解析不同模型的思路。

    处理数组

    现在,我们有如下格式JSON:

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

    这里是一个数组,并且其大小写各不相同。此时我们不需要任何CodingKey,只需使用unkeyed container

    打开本节代码Unkeyed containers,添加下面的代码到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和之前用到的KeyedEncodingContainer相似,但是它不需要CodingKey,因为它将编码数据写入JSON数组中。这里我们编码了3中不同的字符串到其中。

    继续解码:

    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)
      }
    }
    let sameLabel = try decoder.decode(Label.self, from: labelData)
    

    这里主要是获取decoder.unkeyedContainer,获取容器中最后一个值来初始化name

    处理嵌套在对象中的数组

    现在我们有如下格式JSON:

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

    这次,标签对应在了label字段下。我们需要使用nested unkeyed containers去进行编解码。

    打开本节代码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)
    }
    

    这里我们创建了一个nested unkeyed container,并填充了3个字符串。运行代码,并查看string的值,可以看到预期结果。

    继续添加下面的代码进行解码:

    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)
    

    这里,我们像之前一样,使用unkeyed container的最后一个值初始化label字段,只不过获取的是嵌套的容器。

    处理可选字段

    最后,我们的模型中的属性也可以是可选类型,container也提供了对应的编解码方法:

    encodeIfPresent(value, forKey: key)
    decodeIfPresent(type, forKey: key)
    

    总结

    今天我们由浅入深的学习了如何在Swift中处理JSON。其中自定义Coding keys处理子类等部分需要重点理解。希望对大家有所帮助。

    原文链接:
    https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift

    相关文章

      网友评论

          本文标题:Swift&JSON 从入门到精通

          本文链接:https://www.haomeiwen.com/subject/vulbhrtx.html