美文网首页
如何自定义model对象的编码过程

如何自定义model对象的编码过程

作者: 醉看红尘这场梦 | 来源:发表于2020-03-27 10:59 被阅读0次

    虽然绝大多数时候,我们让一个类型遵从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
        }
    }
    
    

    然后,创建一个durationnil的对象:

    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)的应用场景。

    相关文章

      网友评论

          本文标题:如何自定义model对象的编码过程

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