Swift中的NSCoding和Codable

自定义对象的保存是程序中的一个重要功能,ObjC中提供了 NSCoding/NSSecureCoding 协议供对象实现以满足archive和uncharive的要求。因为在ObjC中,绝大多数情况都是使用继承自NSObject的类。但是在Swift中,struct类型占据了重要的地位,接下来会说明Swift中:

  1. NSCoding的使用
  2. Codable的使用
  3. 实际使用案例

本文所用版本为Swift4.x。

NSCoding

NSCoding在ObjC中就已经存在,在Swift的定义和使用和ObjC中一样,它的主要功能是归档对象,便于存储。实现NSCoding协议必须实现两个函数:

required init(coder aDecoder: NSCoder) {
      name = aDecoder.decodeObject(forKey: "name") as? String
      self.init(name)

  }

  func encode(with aCoder: NSCoder) {
      aCoder.encode(name, forKey: "name")
  }

只有实现了NSCoding协议的类对象,才可以使用 NSKeyedArchiver.archivedData 归档成文件然后进行存储。注意,只有class才能实现该协议。

Codable

Codable是Swift中独有的协议,使用于各种自定义的类型(class和struct都可以实现该协议)。它的主要作用是使自定义的数据可以表示成各种外部数据表现形式,如常见的JSON。当我们需要向服务器发送数据,存储成文件时都需要自定义数据类型可以被编码和解码。Codable的定义:

public typealias Codable = Decodable & Encodable

可以发现,Swift中把编码和解码分别定义成单独的协议,也就是说根据实际情况,可以只实现 Encodable或Decodable。
常见的String,Int,Array(如果元素实现了Codable)等结构体和Foundation标准库中的Date,Data等类型都实现了Codable。如果自定义类型的属性实现了Codable的话,那自定义的类型也就默认实现了Codable,唯一需要做的就是声明实现Codable,如:

struct MyPerson: Codable {
  var name: String
  var age: Int
}

那对形如下面结构的json对象可以进行编码/解码

[ { "name": "Sun", "age": 20 }, { "name": "John", "age": 19 } ]

//decode let jsonData = jsonString.data(using: .utf8)! let persons = try JSONDecoder().decode([MyPerson].self, from: jsonData) //encode var encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let encodeData = try encoder.encode(persons)

这样,通过JSONEncode和JSONDecode就实现了对象到Data的转化

以上是Codable的基本概念和用法。

key名字不一致

有时候json schema中的key和定义的struct中的属性名不一致或者为了避免和关键字重名,Swift中引入了CodingKey协议,示例如下:

//假设JSON数据变为:
[
  {
      "student_name": "Sun",
      "age": 20
  },
  {
      "student_name": "John",
      "age": 19
  }
]
//相应的增加枚举类型Keys,其raw value为对应的key,如果一致可以省略
struct MyPerson: Codable {
  var name: String
  var age: Int
  private enum CodingKeys: String,CodingKey{
    case name = "student_name"
    case age
  }
}

注意,如果没有实现Codable定义的函数时,enum名字必须为 CodingKeys

属性深度不一致

有时候收到的json数据如:

[ { "name":"Suny Island", "foundingYear":1976, "latitude": 102.9870, "longitude": 32.9870 }, { "name":"Green park", "foundingYear":2008, "latitude": 32.9870, "longitude": 27.9870 } ]

而定义的模型:

struct Coordinate: Codable {
  var latitude: Double
  var longitude: Double
}

struct Landmark: Codable {
  var name: String
  var founding: Int
  var location: Coordinate
}

这时我们就需要实现Encodable和Decodable协议--当然,可以根据实际情况只实现Decodable。需要添加的部分:

private enum CodingKeys:String, CodingKey {
    case name
    case founding = "foundingYear"
    case latitude
    case longitude
}

init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    founding = try values.decode(Int.self, forKey: .founding)
    location = try Coordinate(from: decoder)
}
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(founding, forKey: .founding)
    try container.encode(location.latitude, forKey: .latitude)
    try container.encode(location.longitude, forKey: .longitude)
}

若返回的json中某个属性为null,那么在相应的模型中使其变为 optional,并且使用 try? 。
如果是相反的情形,如:

[ { "name":"Suny Island", "detail":{ "foundingYear":1976, "latitude": 102.9870, "longitude": 32.9870 } }, { "name":"Green park", "detail": { "foundingYear":2008, "latitude": 32.9870, "longitude": 27.9870 } } ]

而定义的模型:

struct Landmark: Codable {
  var name: String
  var foundingYear: Int
  var latitude: Double
  var longitude: Double
}

这种情况下需要用到nestedContainer:

private enum CodingKeys:String, CodingKey {
    case name
    case detail

}
private enum DetailKeys: String, CodingKey{
    case foundingYear
    case latitude
    case longitude
}

init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    let detailInfo = try values.nestedContainer(keyedBy: DetailKeys.self, forKey: .detail)
    foundingYear = try detailInfo.decode(Int.self, forKey: .foundingYear)
    latitude = try detailInfo.decode(Double.self, forKey: .latitude)
    longitude = try detailInfo.decode(Double.self, forKey: .longitude)
}
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    var detailInfo = container.nestedContainer(keyedBy: DetailKeys.self, forKey: .detail)
    try detailInfo.encode(foundingYear, forKey: .foundingYear)
    try detailInfo.encode(latitude, forKey: .latitude)
    try detailInfo.encode(longitude, forKey: .longitude)
}

以上两种情形需要好好体会。

参考链接

Encoding and Decoding Custom Types
Using JSON with Custom Types
示例代码