代码之家  ›  专栏  ›  技术社区  ›  ABeard89

Swift可编码:如何将顶级数据编码到嵌套容器中

  •  3
  • ABeard89  · 技术社区  · 8 年前

    我的应用程序使用的服务器返回的json如下所示:

    {
        "result":"OK",
        "data":{
    
            // Common to all URLs
            "user": {
                "name":"John Smith" // ETC...
            },
    
            // Different for each URL
            "data_for_this_url":0
        }
    }
    

    正如您所看到的,特定于URL的信息与公共资源存在于同一个词典中 user 字典。

    目标:

    1. 将此JSON解码为类/结构。
      • 因为 用户 是常见的,我希望它位于顶级类/结构中。
    2. 编码为新格式(例如plist)。
      • 我需要保留原来的结构。(即重新创建 data 顶级词典 用户 信息和子对象的信息)

    问题:

    重新编码数据时,无法同时写入 用户 字典(从顶级对象)和特定于URL的数据(从子对象)到编码器。

    要么 用户 覆盖其他数据或其他数据覆盖 用户 . 我不知道如何组合它们。

    以下是我目前为止的情况:

    // MARK: - Common User
    struct User: Codable {
        var name: String?
    }
    
    // MARK: - Abstract Response
    struct ApiResponse<DataType: Codable>: Codable {
        // MARK: Properties
        var result: String
        var user: User?
        var data: DataType?
    
        // MARK: Coding Keys
        enum CodingKeys: String, CodingKey {
            case result, data
        }
        enum DataDictKeys: String, CodingKey {
            case user
        }
    
        // MARK: Decodable
        init(from decoder: Decoder) throws {
            let baseContainer = try decoder.container(keyedBy: CodingKeys.self)
            self.result = try baseContainer.decode(String.self, forKey: .result)
            self.data = try baseContainer.decodeIfPresent(DataType.self, forKey: .data)
    
            let dataContainer = try baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
            self.user = try dataContainer.decodeIfPresent(User.self, forKey: .user)
        }
    
        // MARK: Encodable
        func encode(to encoder: Encoder) throws {
            var baseContainer = encoder.container(keyedBy: CodingKeys.self)
            try baseContainer.encode(self.result, forKey: .result)
    
            // MARK: - PROBLEM!!
    
            // This is overwritten
            try baseContainer.encodeIfPresent(self.data, forKey: .data)
    
            // This overwrites the previous statement
            var dataContainer = baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
            try dataContainer.encodeIfPresent(self.user, forKey: .user)
        }
    }
    

    例子:

    在下面的示例中,重新编码的plist不包括 order_count ,因为它被包含 用户 .

    // MARK: - Concrete Response
    typealias OrderDataResponse = ApiResponse<OrderData>
    
    struct OrderData: Codable {
        var orderCount: Int = 0
        enum CodingKeys: String, CodingKey {
            case orderCount = "order_count"
        }
    }
    
    
    let orderDataResponseJson = """
    {
        "result":"OK",
        "data":{
            "user":{
                "name":"John"
            },
            "order_count":10
        }
    }
    """
    
    // MARK: - Decode from JSON
    let jsonData = orderDataResponseJson.data(using: .utf8)!
    let response = try JSONDecoder().decode(OrderDataResponse.self, from: jsonData)
    
    // MARK: - Encode to PropertyList
    let plistEncoder = PropertyListEncoder()
    plistEncoder.outputFormat = .xml
    
    let plistData = try plistEncoder.encode(response)
    let plistString = String(data: plistData, encoding: .utf8)!
    
    print(plistString)
    
    // 'order_count' is not included in 'data'!
    
    /*
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>data</key>
        <dict>
            <key>user</key>
            <dict>
                <key>name</key>
                <string>John</string>
            </dict>
        </dict>
        <key>result</key>
        <string>OK</string>
    </dict>
    </plist>
    */
    
    2 回复  |  直到 8 年前
        1
  •  3
  •   ABeard89    8 年前

    我刚才在查看编码器协议时有了顿悟。

    KeyedEncodingContainerProtocol.superEncoder(forKey:) 方法正好适用于这种情况。

    此方法返回一个单独的 Encoder 它可以收集多个项目和/或嵌套容器,然后将它们编码为一个键。

    对于这个特定的案例,最高层 user 数据可以通过简单地调用自己的 encode(to:) 方法,使用新的 superEncoder . 然后,也可以使用编码器创建嵌套容器,以便正常使用。

    问题的解决方案

    // MARK: - Encodable
    func encode(to encoder: Encoder) throws {
    
        var baseContainer = encoder.container(keyedBy: CodingKeys.self)
        try baseContainer.encode(self.result, forKey: .result)
    
        // MARK: - PROBLEM!!
    //    // This is overwritten
    //    try baseContainer.encodeIfPresent(self.data, forKey: .data)
    //
    //    // This overwrites the previous statement
    //    var dataContainer = baseContainer.nestedContainer(keyedBy: DataDictKeys.self, forKey: .data)
    //    try dataContainer.encodeIfPresent(self.user, forKey: .user)
    
        // MARK: - Solution
        // Create a new Encoder instance to combine data from separate sources.
        let dataEncoder = baseContainer.superEncoder(forKey: .data)
    
        // Use the Encoder directly:
        try self.data?.encode(to: dataEncoder)
    
        // Create containers for manually encoding, as usual:
        var userContainer = dataEncoder.container(keyedBy: DataDictKeys.self)
        try userContainer.encodeIfPresent(self.user, forKey: .user)
    }
    

    输出:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>data</key>
        <dict>
            <key>order_count</key>
            <integer>10</integer>
            <key>user</key>
            <dict>
                <key>name</key>
                <string>John</string>
            </dict>
        </dict>
        <key>result</key>
        <string>OK</string>
    </dict>
    </plist>
    
        2
  •  -2
  •   decybel    8 年前

    很好的问题和解决方案,但如果您想简化它,可以使用 KeyedCodable 我写的。您的代码表的整个实现过程将是这样的(当然,orderdata和user保持不变):

    struct ApiResponse<DataType: Codable>: Codable, Keyedable {
      // MARK: Properties
      var result: String!
      var user: User?
      var data: DataType?
    
      mutating func map(map: KeyMap) throws {
          try result <-> map["result"]
          try user <-> map["data.user"]
          try data <-> map["data"]
      }
    
      init(from decoder: Decoder) throws {
          try KeyedDecoder(with: decoder).decode(to: &self)
     }
    

    }