macOS 开发之 Swift 的 Codable

近日研究了一下如何将自定义类型数据持久化,在研究过程中发现 Swift 的 Codable 真的很方便,觉得有必要写一写这个 Codable,在本文一起探讨一下以下三个方面:

  • 什么是 Swift 的 Codable
  • 怎么使用 Codable
  • Codable 给我们带来什么便利

开发平台

  • macOS 10.14.4
  • Swift 5
  • xcode 10.2

Swift 的 Codable

In a nutshell, Encoding is the process of transforming your own custom type, class or struct to external data representation type like JSON or plist or something else & Decoding is the process of transforming external data representation type like JSON or plist to your own custom type, class or struct.

可以使用的协议

Swift 的标准库中包含了用于自定义类型(结构体、类)与其它表示形式(JSON、Property List 或 二进制)的数据之间相互转换的协议:

  • Encodable: 用于自定义类型向 JSON 或 Property List 的转换,协议包含一个方法

    encode(to:)
  • Decodable: 用于自 JSON 或 Property List 数据向自定义类型的转换,协议包含一个方法

    init(from:)
    
  • Codable: 包含 Encodable 和 Decodable 两方面的转换,其定义如下:

    typealias Codable = Decodable & Encodable
    

使用 Codable 协议 的 Encodable 和 Decodable,可以让我们轻松实现自定义数据类型的序列化以及得到相应数据类型的实例对象。按照字面意思,我们后面将数据类型实例向 JSON 或 Property List 转换的过程称为编码,反之,称为解码!

遵循协议的类型

如果我们想要实现自定义类型或数据模型的编码和解码,必须遵循 Codable 协议!Swift 基本的内建类型已经是 Codable 的了,比如 StringIntDoubleDateData。另外像 ArrayDictionaryOptional 也都是遵循 Codable 协议的,可以进行编码和解码。

如下自定义的结构体 Person 和 Team,遵循 Codable 协议,同时结构体的所有属性要么是标准的 Codable 类型,要么包含 Codable 类型:

struct Person : Codable {
    var id: Int
    var name: String
    var age: Int
    var isMale: Bool
}

struct Team: Codable {
    var master: Person
    var memebers: [Person]
}

Codable 类型的编解码

对于 Codable 的类型,需要使用相应的编码器对我们的数据进行编码和解码,具体细节可以参考后面的小节,有两种编码器可用:

  • JSON
    • JSONEncoder: 将 Codable 类型数据编码为 JSON 数据
    • JSONDecoder: 将 JSON 数据解码为指定的 Codable 类型数据
  • Property List
    • PropertyListEncoder: 将 Codable 类型数据编码为 plist 数据
    • PropertyListDecoder: 将 plist 数据解码为指定的 Codable 类型数据

Codable 的使用

下面我们就一起研究一下 Codable 的使用,这里我们只尝试 Codable 类型数据与 JSON 数据的编码解码的实现。

编码和解码

使用 JSONEncoder 编码

非常简单,只需调用 JSONEncoderencode(_:) 方法就能将 Codable 类型转换为 JSON 数据:

let jack = Person(id: 1, name: "Jack", age: 12, isMale: true)
if let jackData = try? JSONEncoder().encode(jack) {
    print(String(data: jackData, encoding: .utf8)!)
}

可以看到打印出转换得到的 JSON 字符串数据:

{"age":12,"id":1,"isMale":true,"name":"Jack"}

是不是很方便,解码同样如此简单!

使用 JSONDecoder 解码

只需要调用 JSONDecoder 实例的 decode(_:from:) 方法就能将 JSON 对象转换得到指定类型的实例。

let jsonString = """
{
    "id": 2,
    "name": "lucy",
    "age": 11,
    "isMale": false
}
"""
if let json = jsonString.data(using: .utf8) {
    let lucy = try? JSONDecoder().decode(Person.self, from: json)
    print(lucy!)
}

打印结果,如您所料,将 JSON 字符串成功的解码为了 Person 实例对象:

Person(id: 2, name: "lucy", age: 11, isMale: false)

Codable 使用的基本步骤

综上过程,我们可以总结一下,要使用 Swift 的特性其实很简单,分两步:

  • 自定义类型遵循 Codable 协议
  • 使用编码器实现自定义类型数据的编码和解码

使用 CodingKeys 来选择部分属性是 Codable 的

也许此时此刻您可能会想:

  • 如果不想把自定义数据所有的属性编码到 JSON 该怎么办?
  • 如果 JSON 数据中的键名与自定义类型中的属性名不一致怎么办?

请放心,您想到的,Apple 同样照顾到了!就是本节要讲的 CodingKeys

Codable types can declare a special nested enumeration named CodingKeys that conforms to the CodingKey protocol. When this enumeration is present, its cases serve as the authoritative list of properties that must be included when instances of a codable type are encoded or decoded. The names of the enumeration cases should match the names you’ve given to the corresponding properties in your type.

CodingKeys 是我们在自定义数据类型中定义的枚举,有以下两点要求:

  • 枚举元素类型是 String,并且遵循 CodingKey 协议、
  • 枚举元素的名称必须与自定义类型中的属性名称保持一致

那么我们回过头来看一下前面的两个问题怎么解决:

  • CodingKeys 中的元素与自定义数据类型中的属性名称对应,只要删除对应属性的枚举元素就可以实现编码时对应属性的忽略,这样就解决了第一个问题,但是要注意,不编码的属性必须赋予默认值
  • 为 CodingKeys 中枚举元素自定义 String 值与 JSON 数据中的键名对应起来,就能解决第二个问题。

假如想为 Person 结构体添加一个 description 的属性,同时不想让它参与编码和解码,另外 JSON 数据中的键名是中文的,我们可以重构 Person 类:

struct Person : Codable {
    var id: Int
    var name: String
    var age: Int
    var isMale: Bool
    var description: String = "person"
    
    enum CodingKeys: String, CodingKey {
        case id = "身份证号"
        case name = "姓名"
        case age = "年龄"
        case isMale
    }
}

我们看一下将自定义数据转换为 JSON 会是怎样的:

let tim = Person(id: 3, name: "tim", age: 10, isMale: true, description: "")
if let timData = try? JSONEncoder().encode(tim) {
    print(String(data: timData, encoding: .utf8)!)
}

编码得到的 JSON 数据如下:

{"姓名":"tim","isMale":true,"年龄":10,"身份证号":3}

自定义 encode 和 decode

我们定义一个 Size 结构体、Point 结构体和 Rect 结构体如下:

struct Size: Codable {
    var width: Double
    var height: Double
}

struct Point: Codable {
    var x: Double
    var y: Double
}

struct Rect: Codable {
    var position: Point
    var size: Size
}

我们利用一开始定义的 Rect 结构体声明一个 rect,坐标在原点,宽高都为 2.0,并将其转换为 JSON 数据:

let rect = Rect(position: Point(x: 0.0, y: 0.0), size: Size(width: 2.0, height: 2.0))
if let rectData = try? JSONEncoder().encode(rect) {
    print(String(data: rectData, encoding: .utf8)!)
}

team 对应的 JSON 字符串如下:

{"position":{"x":0,"y":0},"size":{"width":2,"height":2}}

但是呢,十里不想让 JSON 数据中 x 和 y 嵌套在 positon 中,也不想 width 和 height 嵌套在 size 中,而是像下面的样子:

{
    "x": 0,
    "y": 0,
    "width": 2.0,
    "height": 2.0
}

要实现这种需求,我们必须自定义 Encodable 协议的 encode(_:) 方法 和 Decodable 协议的 init(from:) 方法,实现自定义的编码解码逻辑,大体分下面几步:

  • 定义 CodingKeys 枚举,元素与目标 JSON 数据的键名对应,定义 x 和 y 而不是 position,定义 width 和 height 而不是 size
  • 删除 Rect 定义中的 Codable
  • 扩展 Rect 遵循 Encodable 协议,并实现 encode(_:) 方法
  • 扩展 Rect 遵循 Decodable 协议,并实现 init(from:) 方法

最终 Rect 定义如下:

struct Rect {
    var position: Point
    var size: Size
    
    enum CodingKeys: String, CodingKey {
        case x
        case y
        case width
        case height
    }
}

extension Rect: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(position.x, forKey: .x)
        try container.encode(position.y, forKey: .y)
        try container.encode(size.width, forKey: .width)
        try container.encode(size.height, forKey: .height)
    }
}

extension Rect: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let x = try container.decode(Double.self, forKey: .x)
        let y = try container.decode(Double.self, forKey: .y)
        position = Point(x: x, y: y)
        let width = try container.decode(Double.self, forKey: .width)
        let height = try container.decode(Double.self, forKey: .height)
        size = Size(width: width, height: height)
    }
}

测试一下编码实现:

let rect = Rect(position: Point(x: 0.0, y: 0.0), size: Size(width: 2.0, height: 2.0))
if let rectData = try? JSONEncoder().encode(rect) {
    print(String(data: rectData, encoding: .utf8)!)
}

得到的 JSON 数据的打印结果为:

{"y":0,"x":0,"width":2,"height":2}

测试一下解码的实现:

let rectString = """
{
    "x": 3,
    "y": 3,
    "width": 2.5,
    "height": 2.5
}
"""

if let json = rectString.data(using: .utf8) {
    let newRect = try? JSONDecoder().decode(Rect.self, from: json)
    print(newRect!)
}

得到的 newRect 实例对象打印结果:

Rect(position: __lldb_expr_21.Point(x: 3.0, y: 3.0), size: __lldb_expr_21.Size(width: 2.5, height: 2.5))

Codable 带来的福利

上面说了这么多的 Codable ,到底我们能用它来干什么呢,最主要的两个应用方向就是自定义类型数据持久化和网络通信。

数据持久化

其中一种常用的数据持久化方式就是属性列表(Property List),UserDefaults.standard 适合存储轻量级的本地数据,其提供了与默认数据库相交互的编程接口。其实它存储在应用程序的一个plist文件里,路径为应用沙盒Document目录平级的 /Library/Prefereces 里。另外,其只能存储可以序列化的数据类型,比如我们一开始说的那些基本类型,也就是 Codable 的,所以一旦我们自定义的数据类型遵循了 Codable 协议,即可序列化了,那我们的自定义类型的数据可以自由存取了。

注意:

  • 使用这种方式一般用于存储应用程序的配置信息
  • 手动调用 synchronize 方法可以立马将数据持久化存储

这里只是以这种数据持久化为例,讲一下如何持久化自定义数据类型的数据:

let rectDemo = Rect(position: Point(x: 1.0, y: 1.0), size: Size(width: 2.0, height: 2.0))
if let demoData = try? JSONEncoder().encode(rectDemo) {
    UserDefaults.standard.set(demoData, forKey: "rect-test")
    UserDefaults.standard.synchronize()
    print("saved successfully!")
}

if let rectJson = UserDefaults.standard.value(forKey: "rect-test") as? Data {
    let newRect = try? JSONDecoder().decode(Rect.self, from: rectJson)
    print(newRect!)
}

可以看到打印结果:

saved successfully!
Rect(position: __lldb_expr_29.Point(x: 1.0, y: 1.0), size: __lldb_expr_29.Size(width: 2.0, height: 2.0))

网络通信

HTTP/HTTPS 网络通信中 JSON 是常用的交互数据类型,假如我们编写一个网络接口,当外部请求的时候,我们为其返回一个响应,如果我们定义一个 Codable 的 Response 类型,可以方便生成响应数据:

struct Response: Codable {
    var ststus: Int
    var message: String
}

let res = Response(ststus: 200, message: "OK")
let resData = try? JSONEncoder().encode(res)

总结

Swift 4 开始支持的 Codable 大大简化了对自定义类型数据序列化的实现,相信您会用得到!