Codable

Codable

Codable 在 Swift4.0 提出时带来的, 可以说是解决了一个大痛点…(虽然之前能用一些工具减轻写模板代码的痛苦)
Codable 是 Decodable 和 Encodable 的结合, 支持 class, struct, enum,
typealias Codable = Decodable & Encodable

Encodable
encode(to:) 解析类型的值到给定的encoder

Decodable
init(from:) 从一个外部类型转化到内部定义的类型

在开发中我们做的最多的是就是将网络请求返回的数据转化为我们的modal, 这里如果你定义的属性是常规的类型, 他们已经原生支持了Codable 协议, 所以在这里不需要特殊的处理, 只需要下面这段代码, 就能将你获取到的 json 数据转换为定义的 modal 类型

Swift 已经为我们内建了一些已经实现了 Codable 的数据:
String, Int, Double, Data, URL

同时, Array, Dictionary, Optional 如果里面的元素是符合 Codable 的类型的话, 那整个也是可以符合 Codable 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Foundation
struct Swifter: Decodable {
let fullName: String
let id: Int
let twitter: URL
}

let json = """
{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
""".data(using: .utf8)!

// our data in native (JSON) format
let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // Decoding our data
print(myStruct) // decoded!!!!!

这里其实 Decodable 是需要实现一个 init(from: Decoder) 的方法的. 但是我们没有写, 因为编译器会自动帮我们生成一个.

而且这个 decode 支持嵌套解析, 只要属性和返回的 key 值对应, 且自定义类型也是遵守Codable(至少在解析方面遵守Decodable) 就可以自动解析了~

自定义解析

然而…生活仍然一如既往的不美好…

有时候你需要忽略某些 key 的解析, 或者某些返回的字段和你定义的 propertyName 不一样.
后端返回, 很懒的有些…直接是返回数据库的字段类型回来, 不是驼峰标识, 而是有下划线的, 这与我们的编码风格就会有出入, 不处理的话, 在这一块的代码上, 就会显得很乱.

这时候我们就需要自定定义一些东西, 而不是让编译器做其默认做的事情.

enum CodingKeys

Codable 类型可以在内部声明一个名叫DodingKeys 的枚举, 其遵守CodingKey协议. 当这个枚举被定义的时候, 其中定义的 case 必须是在类中存在的.

Decoder 负责处理 JOSN 和 Plist 的解析工作, 其中有两个方法

  1. 它是一个 Raw Type – String, CodingKey
  2. case 的名字必须和 propertyName 一样
  3. 在 CodingKeys 中忽略你想忽略的 property, 但是对于忽略的这个属性需要有一个默认值.
  4. 如果转换时 key 和你定义的对应不上, 你可以在枚举这里指定其对应的 key.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Photo: Codable
{
//...Other properties (described in Code Snippet - 1)...

//This property is not included in the CodingKeys enum and hence will not be encoded/decoded.
var format: String = "png"

enum CodingKeys: String, CodingKey
{
case title = "name"
case url = "link"
case isSample
case metaData
case type
case size
}
}

容器

1
2
public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
public func singleValueContainer() throws -> SingleValueDecodingContainer

看过之前的 keynote 的话, 可以知道, 这个 Coder 包含了键值对容器: KeyedContainer, 非键值对容器: UnKeyedContainer, 还有单个值的容器: SingleValueDecodingContainer,

这里我们比较需要的就是对于键值对的解析, 我们需要告诉它是什么 key.

Decoder提供了基础的功能解析原始数据,自定义数据就需要我们自己来搞定。
KeyedDecodingContainer:我们的容器是通过键值匹配的,所以大可以看作[Key: Any]这样的字典结构。
不同的键对应不同的类型数据,所以容器提供的不同解码方法:decode(Type:forKey:)。
它的神奇之处就在于容器会自动匹配数据类型。
当然,解码器也提供了通用方法:

1
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T : Decodable

自定义解析的实现

我们不想通过默认的方式去解析的话, 我们就需要自己指定解析的映射: init(from: Decoder), 这个默认是在 Swift 编译器会自己实现

  1. 选择解码器: 这里我们解析网络请求常用的是 JSON , 用 JSONDecoder. (JSON 和 Plist 解析器都是系统内置的, 有更特殊需求的可以自己实现, 步奏如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//   这里我们有数据:
{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
// 对应一个 model, 遵守 CodignKey 协议
enum MyStructKeys: String, CodingKey {
case fullName = "fullName"
case id = "id"
case twitter = "twitter"
}
// 创建一个容器
let container = try decoder.container(keyedBy: MyStructKeys.self)
// 提取数据
// 这里我们需要做类型转换:
let fullName: String = try container.decode(String.self, forKey: .fullName)
let id: Int = try container.decode(Int.self, forKey: .id)
let twitter: URL = try container.decode(URL.self, forKey: .twitter)

// 第四步:初始化
// 使用默认的构造器
let myStruct = Swifter(fullName: fullName, id: id, twitter: twitter)

现在我们就来看看全部实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Foundation
struct Swifter {
let fullName: String
let id: Int
let twitter: URL

init(fullName: String, id: Int, twitter: URL) { // default struct initializer
self.fullName = fullName
self.id = id
self.twitter = twitter
}
}
extension Swifter: Decodable {
enum MyStructKeys: String, CodingKey { // declaring our keys
case fullName = "fullName"
case id = "id"
case twitter = "twitter"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MyStructKeys.self) // defining our (keyed) container
let fullName: String = try container.decode(String.self, forKey: .fullName) // extracting the data
let id: Int = try container.decode(Int.self, forKey: .id) // extracting the data
let twitter: URL = try container.decode(URL.self, forKey: .twitter) // extracting the data

self.init(fullName: fullName, id: id, twitter: twitter) // initializing our struct
}
}

let json = """
{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
""".data(using: .utf8)! // our native (JSON) data

let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // decoding our data
print(myStruct) // decoded!

注意在这里, 对于遵守 Codable 的继承, 其子类并不能直接正确解析出想要的效果, 还需要进行自定义编解码的处理

还有一种是, 你定义的类型的结构, 和返回的结构不一样, 但是你不变结构, 对其进行适配. 同样的我们需要自己实现这两个协议中的方法. 而且对 CodingKeys 做一些改变:

如我们原来有这么个 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Photo: Codable
{
var title: String
var size: Size

enum CodingKeys: String, CodingKey
{
case title = "name"
case size
}
}

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

本来结构都对应上了的话, 我们可以直接转化, 不用多谢代码的. 但是现在我们的返回变成了这样:

1
2
3
4
5
{
"title":"Apple",
"width":150,
"height":150
}

这时候, 我们:

  1. 用 width 和 height 替换 size
  2. 移除 Photo 后的 Codalble 协议
  3. 单独创建 extension 实现 Encodable 和 Codable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct Photo
{
var title: String
var size: Size

enum CodingKeys: String, CodingKey
{
case title = "name"
case width
case height
}
}

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

extension Photo: Decodable
{
init(from decoder: Decoder) throws
{
let values = try decoder.container(keyedBy: CodingKeys.self)
title = try values.decode(String.self, forKey: .title)
let width = try values.decode(Double.self, forKey: .width)
let height = try values.decode(Double.self, forKey: .height)
size = Size(width: width, height: height)
}
}

语法上的局限

  • 不能在 extension 上直接声明 一个组合类型的协议如: Codable
  • 只能使用一个具体类型来进行 encode & decode, 不能使用一个抽象类型或者协议什么的..(Codable 是一个 alias, 它并不会生成一个类型来指定你的使用. 但是可以在进行一个封装, 使用泛型来限定好类型. 参考)

对于代码生成和 protocol 的结合

比如上面的例子, 当我们不override init 函数时, 编译器会自动帮我们实现一个, 并且带有默认的行为. 但是有时候, 他的默认行为不符合我们的要求, 但是又不想去 override 这个的时候, 在 OC, 我们可以通过 runtime 使用 swizzle 方法来混淆这个操作, 但在 Swift, 使用 runtime 是很有限的. 我们需要想一个符合 Swift 特性的方法去实现这个不 override init 而达成我们的目标.

比如说, init(from:) 里, 默认会将 decoder 创建的 container 去调用其 decodeIfPresent(_ type: Int.Type, forKey key: CodingKey) -> Int?这个函数, 其不符合我们的需求的地方就在这里, 那我们就重写这个函数. 在 Swift 的函数调用规则里, 在 Module 下的函数会优先被调用, 这样, 我们就通过在本文件声明 extension 并重新实现这个函数, 那么这里调用的就会是我们实现的函数. 如果不想被外界使用, 还能将其声明为 fileprivate, 这样其他文件也不会被干扰到了.

ref: 或许你并不需要重写 init(from:) 方法

ref:
Everything about Codable in Swift 4
Swift4 终极解析方案:基础篇

Ultimate Guide to JSON Parsing with Swift 4