虽然绝大多数时候,我们让一个类型遵从Codable就可以享受它提供的编码和解码便利,但终有一些时候,我们需要改变其中一些默认的规则,例如我们在最初看到的关于日期和时间的编码。为此,在这一节,我们将走到Codable内部,来定制其中的方法。
自定义Encoding
为了演示这个过程,我们先来看在第一个视频中编码日期时间的问题。这里,我们对Episode进行了一些简化:
struct Episode: Codable {
let title: String
let createdAt: Date
enum CodingKeys: String, CodingKey {
case title
case createdAt = "created_at"
}
}
然后,当我们编码一个Episode对象的时候:
let episode = Episode(
title: "How to parse a JSON - III",
createdAt: Date())
let encoder = JSONEncoder()
let data = try! encoder.encode(episode)
dump(String(data: data, encoding: .utf8)!)
就会得到这样的结果:
"{\"title\":\"How to parse a JSON - III\",\"created_at\":525248629.550177}"
为了自定义Episode的编码过程,我们要实现下面这个方法:
extension Episode {
func encode(to encoder: Encoder) throws {
// Todo: Add the custom encoding here
}
}
接下来,Encoder中包含了几种不同的容器,我们要做的,就是根据要编码的内容,选择对应的容器,并把对应的值编码进去。例如:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
}
在encode的实现里,我们使用的容器叫做Keyed Container。因为,要编码的内容同时包含了key和value,我们仍旧通过CodingKeys.self告诉容器model中的属性和JSON中的key的对应规则。另外,由于我们要向container中添加内容,它应该必须是一个变量。
有了容器之后,就是把每个属性编码到容器里:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(createdAt, forKey: .createdAt)
}
现在重新执行一下,会发现,结果和之前是相同的。这很正常,因为当我们什么都不写的时候,Codable中默认的encode实现,和我们自己写的是一样的。
现在,要定制Date的编码方式,为此,我们得使用另外一种容器,叫做Single Value Container:
encoder.dateEncodingStrategy = .custom({ (date, encoder) in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd hh:mm:ss"
let stringData = formatter.string(from: date)
var container = encoder.singleValueContainer()
try container.encode(stringData)
})
当我们把.dateEncodingStrategy改成.custom之后,它的关联值,便是一个closure。这个closure的第一个参数,表示要编码的Date对象,第二参数,是Encoder对象,在这个clousre的实现里,最关键的,是最后两行:
var container = encoder.singleValueContainer()
try container.encode(stringData)
这样,我们就用Single Value Container自定义了Date对象的编码格式。重新执行一下,就会看到这样的结果:
- "{\"title\":\"How to parse a JSON - III\",\"created_at\":\"2017-08-24 08:14:56\"}"
自定义Optional对象的编码
了解了encode的实现之后,我们来看一个特殊情况,当处理Optional类型的属性时,如果属性为nil,就不会出现在默认的结果里了。例如,我们把Episode改成这样:
struct Episode: Codable {
let title: String
let createdAt: Date
let comment: String?
enum CodingKeys: String, CodingKey {
case title
case createdAt = "created_at"
case comment
}
}
然后,创建一个duration为nil的对象:
let episode = Episode(
title: "How to parse a JSON - III",
createdAt: Date(),
comment: nil)
/// ...
let data = try! encoder.encode(episode)
dump(String(data: data, encoding: .utf8)!)
这是,我们可以先把自定义的encode注释掉,重新执行就会发现,comment并不在里面:
"{\"title\":\"How to parse a JSON - III\",\"created_at\":\"2017-08-24 09:40:58\"}"
这是因为,在默认的实现里,对于Optional类型,默认是用encodeIfPresent进行编码的,只有Optional不为nil,值才会编码到结果里。但通常,这个行为不是我们想要的,为此,我们可以在自定义的encode里强行把它编码进来:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(comment, forKey: .comment)
}
重新执行一下,就能看到结果了:
"{\"title\":\"How to parse a JSON - III\",\"created_at\":\"2017-08-24 10:11:40\",\"comment\":null}"
至此,我们就已经了解了两种“容器”了,接下来,我们看如何自定义数组类型的编码,并以此了解第三种容器的用法。
自定义数组类型的编码
之前我们提到过,在JSON里,value除了一个简单值之外,还可以是一个数组。例如,我们给Episode添加一个包含视频切片时间的数组slices以及视频的总时长duration:
struct Episode: Codable {
/// ...
let duration: Int
let slices: [Float] = [0.25, 0.5, 0.75]
enum CodingKeys: String, CodingKey {
/// ...
case slices
}
}
其中,slices表示在duration的25% / 50% / 75%的位置,对视频进行切片。当我们默认对slices编码的时候,这些百分比数字就会直接包含在JSON里。
如果我们希望提交到服务器的JSON中包含的是切片实际的时长,就可以在encode里自定义数组的编码过程:
func encode(to encoder: Encoder) throws {
/// ...
var unkeyedContainer =
container.nestedUnkeyedContainer(forKey: .slices)
try slices.forEach {
try unkeyedContainer.encode(Float(duration) * $0)
}
}
这里,由于slices是JSON的一部分,因此,我们使用了nestedUnkeyedContainer方法,创建了一个Nested Unkeyed Container。然后,只要遍历数组中的每个成员,根据duration计算对应的时间,并把时间编码进Unkeyed Container就好了。
然后:
let episode = Episode(
title: "How to parse a JSON - III",
createdAt: Date(),
comment: nil,
duration: 500,
slices: [0.25, 0.5, 0.75)
let data = try! encoder.encode(episode)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
/// ...
print(String(data: data, encoding: .utf8)!)
重新执行下,编码过的slices中,包含的就是对应切片的时长了:
{
"slices" : [
125,
250,
375
],
"title" : "How to parse a JSON - III",
"comment" : null,
"created_at" : "2017-09-03 10:51:43"
}
以上,就是和自定义对象编码有关的内容。其中最重要的,就是要理解容器的概念,以及三种不同容器(keyed container / unkeyed container / single value container)的应用场景。










网友评论