Swift数据解析(第二篇) - Codable 上篇

一. Codable的简单使用

CodableEncodableDecodable 协议的聚合。 同时拥有序列化和反序列化的能力。

swift 复制代码
 public protocol Encodable {
     func encode(to encoder: Encoder) throws
 }
 ​
 public protocol Decodable {
     init(from decoder: Decoder) throws
 }
 ​
 public typealias Codable = Decodable & Encodable

使用起来很简单:

swift 复制代码
 struct Feed: Codable {
    var name: String
    var id: Int
 }
 ​
 let dict = [
    "id": 2,
    "name": "小明",
 ] as [String : Any]
 ​
 let jsonStr = dict.bt_toJSONString() ?? ""
 guard let jsonData = jsonStr.data(using: .utf8) else { return }
 let decoder = JSONDecoder()
 do {
    let feed = try decoder.decode(Feed.self, from: jsonData)
    print(feed)
 } catch let error {
    print(error)
 }
 // Feed(name: "小明", id: 2)

开始使用codeable,感觉一切都很美好。带着这份美好,开始学习Codable。

为了介绍Codable协议,写了挺多演示代码,为了减少示例中的代码量,封装了编码和解码的方法,演示代码将直接使用这四个方法。

swift 复制代码
 extension Dictionary {
    /// 解码
    public func decode<T: Decodable>(type: T.Type) -> T? {
        do {
            guard let jsonStr = self.toJSONString() else { return nil }
            guard let jsonData = jsonStr.data(using: .utf8) else { return nil }
            let decoder = JSONDecoder()
            let obj = try decoder.decode(type, from: jsonData)
            return obj
        } catch let error {
            print(error)
            return nil
        }
    }
     
    /// 字典转json字符串
    private func toJSONString() -> String? {
        if (!JSONSerialization.isValidJSONObject(self)) {
            print("无法解析出JSONString")
            return nil
        }
        do {
            let data = try JSONSerialization.data(withJSONObject: self, options: [])
            let json = String(data: data, encoding: String.Encoding.utf8)
            return json
        } catch {
            print(error)
            return nil
        }
    }
 }
 ​
 ​
 extension String {
    /// 解码
    public func decode<T: Decodable>(type: T.Type) -> T? {
        guard let jsonData = self.data(using: .utf8) else { return nil }
         
        do {
            let decoder = JSONDecoder()
            let feed = try decoder.decode(type, from: jsonData)
            return feed
        } catch let error {
            print(error)
            return nil
        }
    }
 }
 ​
 ​
 extension Encodable {
    /// 编码
    public func encode() -> Any? {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        do {
            let data = try encoder.encode(self)
            guard let value = String(data: data, encoding: .utf8) else { return nil }
            return value
        } catch {
            print(error)
            return nil
        }
    }
 }

二. Decodable

Decodable 是一个协议,提供的 init 方法中包含 Decoder 协议。

swift 复制代码
 public protocol Decodable {
    init(from decoder: Decoder) throws
 }
swift 复制代码
 public protocol Decoder {
    // 在解码过程中达到这一点所采用的编码路径.
    var codingPath: [CodingKey] { get }
     
    // 设置的用于解码的任何上下文信息. 
    var userInfo: [CodingUserInfoKey : Any] { get }
  
    // 生成有键解码容器
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
     
    // 生成无键解码容器
    func unkeyedContainer() throws -> UnkeyedDecodingContainer
    
    // 生成单解码容器
    func singleValueContainer() throws -> SingleValueDecodingContainer
 }

1. DecodingError

解码失败,抛出异常DecodingError。

swift 复制代码
 public enum DecodingError : Error {
     
    /// 出现错误时的上下文
    public struct Context : Sendable {
 ​
        /// 到达解码调用失败点所采取的编码密钥路径.
        public let codingPath: [CodingKey]
 ​
        /// 错误的描述信息
        public let debugDescription: String
 ​
        public let underlyingError: Error?
 ​
        public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = nil)
    }   
 ​
    // 表示类型不匹配的错误。当解码器期望将JSON值解码为特定类型,但实际值的类型与期望的类型不匹配时,会引发此错误。
    case typeMismatch(Any.Type, DecodingError.Context)
 ​
    // 表示找不到值的错误。当解码器期望从JSON中提取某个值,但该值不存在时,会引发此错误。
    case valueNotFound(Any.Type, DecodingError.Context)
 ​
    // 表示找不到键的错误。当解码器期望在JSON中找到某个键,但在给定的数据中找不到该键时,会引发此错误。
    case keyNotFound(CodingKey, DecodingError.Context)
 ​
    // 表示数据损坏的错误。当解码器无法从给定的数据中提取所需的值时,会引发此错误。比如解析一个枚举值的时候。
    case dataCorrupted(DecodingError.Context)
 }

错误中包含许多有用信息:

  • 解码的key
  • 解码时候的上下文信息
  • 解码的类型

这些信息,将为我们的解码失败的兼容提供重要帮助,在下一篇章关于SmartCodable实现中体现价值。

2. 三种解码容器

  • KeyedDecodingContainer: 用于解码包含键值对的JSON对象,例如字典。它提供了一种访问和解码特定键的值的方式。
  • UnkeyedDecodingContainer: 用于解码无键的JSON数组。例如数组,它提供了一种访问下标解码值的方式。
  • SingleValueDecodingContainer:用于解码单个值的JSON数据,例如字符串、布尔值。它提供了一种访问和解码单个值的方式。

SingleValueDecodingContainer

在Swift中,SingleValueDecodingContainer是用于解码单个值的协议。它提供了一些方法来解码不同类型的值,例如字符串、整数、浮点数等。

SingleValueDecodingContainer没有设计decodeIfPresent方法的原因是,它的主要目的是解码单个值,而不是处理可选值。它假设解码的值始终存在,并且如果解码失败,会抛出一个错误。

swift 复制代码
 public protocol SingleValueDecodingContainer {
 ​
    var codingPath: [CodingKey] { get }
 ​
    /// 解码空值时返回true
    func decodeNil() -> Bool
 ​
    /// 解码给定类型的单个值。
    func decode<T>(_ type: T.Type) throws -> T where T : Decodable
 }

使用起来也很简单。如果知道元素的类型,可以使用decode(_:)方法直接解码。如果不知道元素的类型,可以使用decodeNil()方法检查元素是否为nil

python 复制代码
 struct Feed: Decodable {
    let string: String
     
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        string = try container.decode(String.self)
    }
 }
 ​
 let json = """
 "Hello, World!"
 """
 ​
 guard let feed = json.decode(type: Feed.self) else { return }
 print(feed)
 // Feed(string: "Hello, World!")

UnkeyedDecodingContainer

UnkeyedDecodingContainer 是Swift中的一个协议,用于解码无键的容器类型数据,可以按顺序访问和解码容器中的元素,例如数组。可以使用 count 属性获取容器中的元素数量,并使用 isAtEnd 属性检查是否已经遍历完所有元素。

swift 复制代码
public protocol UnkeyedDecodingContainer {

    var codingPath: [CodingKey] { get }

    /// 容器中的元素数量
    var count: Int? { get }

    /// 检查是否已经遍历完所有元素
    var isAtEnd: Bool { get }

    /// 容器的当前解码索引(即下一个要解码的元素的索引)。每次解码调用成功后递增。
    var currentIndex: Int { get }

    /// 解码null值的时候,返回true(前提是必须有这个键)
    mutating func decodeNil() throws -> Bool

    /// 解码指定类型的值
    mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable
    mutating func decode(_ type: Bool.Type) throws -> Bool
    mutating func decode(_ type: String.Type) throws -> String
    mutating func decode(_ type: Double.Type) throws -> Double
    ......

    /// 解码指定类型的值(可选值)
    mutating func decodeIfPresent<T>(_ type: T.Type) throws -> T? where T : Decodable
    mutating func decodeIfPresent(_ type: Bool.Type) throws -> Bool?
    mutating func decodeIfPresent(_ type: String.Type) throws -> String?
    mutating func decodeIfPresent(_ type: Double.Type) throws -> Double?
    ......
  
    /// 解码嵌套的容器类型,并返回一个新的KeyedDecodingContainer
    mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey

    /// 解码嵌套的无键容器类型,并返回一个新的UnkeyedDecodingContainer。
    mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer

    /// 获取父类的容器。
    mutating func superDecoder() throws -> Decoder
}

如果知道元素的类型,可以使用decode(_:forKey:)方法直接解码。可以使用decodeNil()方法检查元素是否为nil

ini 复制代码
struct Feed: Codable {
    var value1: Int = 0
    var value2: Int = 0
    
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        
        if try container.decodeNil() {
            value1 = 0
        } else {
            value1 = try container.decode(Int.self)
        }
        
        if try container.decodeNil() {
            value2 = 0
        } else {
            value2 = try container.decode(Int.self)
        }
    }
}

let json = """
[1, 2]
"""

guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
// Feed(value1: 1, value2: 2)

这个数组数据[1, 2], 按照index的顺序逐个解码。

数据 [1,2] 和模型属性 [value1,value2] 按照顺序一一对应。就形成对应关系 [1 -> value1][2 -> value2]

如果继续解码将报错:

swift 复制代码
init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    
    if try container.decodeNil() {
        value1 = 0
    } else {
        value1 = try container.decode(Int.self)
    }
    
    if try container.decodeNil() {
        value2 = 0
    } else {
        value2 = try container.decode(Int.self)
    }
    
    do {
        try container.decodeNil()
    } catch {
        print(error)
    }
}

// 报错信息
▿ DecodingError
  ▿ valueNotFound : 2 elements
    - .0 : Swift.Optional<Any>
    ▿ .1 : Context
      ▿ codingPath : 1 element
        ▿ 0 : _JSONKey(stringValue: "Index 2", intValue: 2)
          - stringValue : "Index 2"
          ▿ intValue : Optional<Int>
            - some : 2
      - debugDescription : "Unkeyed container is at end."
      - underlyingError : nil

KeyedDecodingContainer

KeyedDecodingContainer 用于解码键值对类型的数据。它提供了一种通过键从容器中提取值的方式。

swift 复制代码
public struct KeyedDecodingContainer<K> : KeyedDecodingContainerProtocol where K : CodingKey {

    public typealias Key = K
    
    public init<Container>(_ container: Container) where K == Container.Key, Container : KeyedDecodingContainerProtocol

    /// 在解码过程中达到这一点所采用的编码密钥路径。
    public var codingPath: [CodingKey] { get }

    /// 解码器对这个容器的所有密钥。
    public var allKeys: [KeyedDecodingContainer<K>.Key] { get }

    /// 返回一个布尔值,该值指示解码器是否包含与给定键关联的值。
    public func contains(_ key: KeyedDecodingContainer<K>.Key) -> Bool

    /// 解码null数据时,返回true
    public func decodeNil(forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool

    /// 为给定键解码给定类型的值。
    public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable
    public func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool
    public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String
    public func decode(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double
    ......

    /// 为给定键解码给定类型的值(如果存在)。如果容器没有值,该方法返回' nil '
    public func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? where T : Decodable
    public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool?
    public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String?
    public func decodeIfPresent(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double?
    ......

    /// 允许您在解码过程中创建一个新的嵌套容器,解码一个包含嵌套字典的JSON对象,以便解码更复杂的数据结构。
    public func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey

    /// 允许您在解码过程中创建一个新的嵌套容器,解码一个包含嵌套数组的JSON对象,以便解码更复杂的数据结构。
    public func nestedUnkeyedContainer(forKey key: KeyedDecodingContainer<K>.Key) throws -> UnkeyedDecodingContainer

    /// 返回一个' Decoder '实例用于从容器中解码' super '。
    /// 相当于calling `superDecoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`.
    public func superDecoder() throws -> Decoder
}

解码动态数据

出行工具的选择有三种: walk,riding,publicTransport。一次出行只会选择其中的一种,即服务端只会下发一种数据。

步行出行:

json 复制代码
{
    "travelTool": "walk",
    "walk": {
        "hours": 3,
    }
}

骑行出行:

json 复制代码
{
    "travelTool": "riding",
    "riding": {
        "hours":2,
        "attention": "注意带头盔"
    }
}

公共交通出行:

json 复制代码
{
    "travelTool": "publicTransport",
    "publicTransport": {
        "hours": 1,
        "transferTimes": "3",
    }
}

通过重写init(from decoder: Decoder) 方法,使用decodeIfPresent处理动态键值结构。

php 复制代码
struct Feed: Decodable {
    
    var travelTool: Tool
    var walk: Walk?
    var riding: Riding?
    var publicTransport: PublicTransport?
    
    enum CodingKeys: CodingKey {
        case travelTool
        case walk
        case riding
        case publicTransport
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        travelTool = try container.decode(Tool.self, forKey: .travelTool)
        walk = try container.decodeIfPresent(Walk.self, forKey: .walk)
        riding = try container.decodeIfPresent(Riding.self, forKey: .riding)
        publicTransport = try container.decodeIfPresent(PublicTransport.self, forKey: .publicTransport)
    }
    
    enum Tool: String, Codable {
        case walk
        case riding
        case publicTransport
    }
    
    struct Walk: Codable {
        var hours: Int
    }
    
    struct Riding: Codable {
        var hours: Int
        var attention: String
    }
    
    struct PublicTransport: Codable {
        var hours: Int
        var transferTimes: String
    }
}

let json = """
{
    "travelTool": "publicTransport",
    "publicTransport": {
        "hours": 1,
        "transferTimes": "3",
    }
}
"""

guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
// Feed(travelTool: Feed.Tool.publicTransport, walk: nil, riding: nil, publicTransport: Optional(Feed.PublicTransport(hours: 1, transferTimes: "3")))

3. keyDecodingStrategy

keyDecodingStrategy 是Swift4.1之后提供的decode策略,用于在解码之前自动更改密钥值的策略。它一个枚举值,提供了三种策略:

less 复制代码
public enum KeyDecodingStrategy : Sendable {

    /// 使用每种类型指定的键,默认策略。
    case useDefaultKeys

    /// 使用小驼峰策略
    case convertFromSnakeCase

    /// 使用自定义策略
    @preconcurrency case custom(@Sendable (_ codingPath: [CodingKey]) -> CodingKey)
}

使用起来也比较简单

swift 复制代码
do {
    let decoder = JSONDecoder()
    /// 默认的
//            decoder.keyDecodingStrategy = .useDefaultKeys
    /// 小驼峰 read_name -> readName
//            decoder.keyDecodingStrategy = .convertFromSnakeCase
    /// 自定义 需要返回CodingKey类型。
    decoder.keyDecodingStrategy = .custom({ codingPath in
        for path in codingPath {
            if path.stringValue == "read_name" {
                return CustomJSONKey.init(stringValue: "readName")!
            } else {
                return path
            }
        }
        return CustomJSONKey.super
    })
    let feed = try decoder.decode(DecodingStrtegyFeed.self, from: jsonData)
    print(feed)
} catch let error {
    print(error)
}

4. 解码异常情况

进行 decode 时候,遇到以下三种情况就会失败。并且只有一个属性解析失败时就抛出异常,导致整个解析失败。

  • 类型键不存在
  • 类型键不匹配
  • 数据值是null

演示样例

css 复制代码
struct Feed: Codable {
    var hobby: Hobby
}

struct Hobby: Codable {
    var name: String
    var year: Int
}

正常的json数据返回是这样的:

python 复制代码
let json = """
{
  "hobby": {
     "name": "basketball",
     "year": 3
  }
}
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)

解码正常,输出为: Feed(hobby: Hobby(name: "basketball", year: 3))

我们将基于这个数据模型,对以下四种特殊数据场景寻找解决方案。

1. 类型键不存在

ini 复制代码
let json = """
{
   "hobby": {}
}
"""

数据返回了空对象。

less 复制代码
▿ DecodingError
  ▿ keyNotFound : 2 elements
    - .0 : CodingKeys(stringValue: "name", intValue: nil)
    ▿ .1 : Context
      ▿ codingPath : 1 element
        - 0 : CodingKeys(stringValue: "hobby", intValue: nil)
      - debugDescription : "No value associated with key CodingKeys(stringValue: "name", intValue: nil) ("name")."
      - underlyingError : nil

解码失败的原因是,Hobby的属性解析失败。缺少Hobby的name属性对应的值。

兼容方案1
csharp 复制代码
struct Hobby: Codable {
    var name: String?
    var year: Int?
}

将Hobby的属性设置为可选类型,会被自动解析为nil。会输出:Feed(hobby: CodableTest.Hobby(name: nil, year: nil))

兼容方案2
swift 复制代码
struct Feed: Codable {
    var hobby: Hobby?
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self.hobby = try container.decode(Hobby.self, forKey: .hobby)
        } catch {
            self.hobby = nil
        }
    }
}

将Hobby设置为可选类型,使用 do-catch。

2. 类型键不匹配

ini 复制代码
let json = """
{
   "hobby": "basketball"
}
"""

数据返回了类型不匹配的值。

swift 复制代码
▿ DecodingError
  ▿ typeMismatch : 2 elements
    - .0 : Swift.Dictionary<Swift.String, Any>
    ▿ .1 : Context
      ▿ codingPath : 1 element
        - 0 : CodingKeys(stringValue: "hobby", intValue: nil)
      - debugDescription : "Expected to decode Dictionary<String, Any> but found a string/data instead."
      - underlyingError : nil

兼容方案:

swift 复制代码
struct Feed: Codable {
    var hobby: Hobby?
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            self.hobby = try container.decode(Hobby.self, forKey: .hobby)
        } catch {
            self.hobby = nil
        }
    }
}

3. 数据值为null

ini 复制代码
let json = """
{
   "hobby": null
}
"""

数据返回了null值。

yaml 复制代码
▿ DecodingError
  ▿ valueNotFound : 2 elements
    - .0 : Swift.Int
    ▿ .1 : Context
      ▿ codingPath : 1 element
        - 0 : CodingKeys(stringValue: "id", intValue: nil)
      - debugDescription : "Expected Int value but found null instead."
      - underlyingError : nil

兼容方案1:

css 复制代码
struct Feed: Codable {
    var hobby: Hobby?
}

将hobby设置为可选值,Codable会自动将null映射为nil,很容易就解决了这个问题。

兼容方案2:

swift 复制代码
struct Feed: Codable {
    var hobby: Hobby?
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        if try container.decodeNil(forKey: .hobby) {
            self.hobby = nil
        } else {
            self.hobby = try container.decode(Hobby.self, forKey: .hobby)
        }
    }
}

判断是否解析为nil。其实就是方案1的逻辑。

这种兼容在商用业务中可以被接受么?

类型键不存在数据值是null 虽然可以通过可选类型避免,但是类型不匹配的情况,只能重写协议来避免。你可以想象一下这种痛苦以及不确定性。

1. 可选绑定的弊端

使用可选势必会有大量的可选绑定,对于 enum 和 Bool 的可选的使用是非常痛苦的。

2. 重写协议方法

重写协议方法,会增加代码量,并且很容易产生错误(如何保证兼容的全面性?),导致解析失败。

一个属性的完整兼容应该包含以下三个步骤,可以想象如果一个模型有上百个字段的场景(对于我们这样做数据业务的app非常常见)。

  • 该字段是否存在 container.contains(.name)
  • 数据是否为null try container.decodeNil(forKey: .name)
  • 是否类型匹配 try container.decode(String.self, forKey: .name)
swift 复制代码
struct Person: Codable {
    var name: String
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        if container.contains(.name) { // 数据中是否否包含该键
            if try container.decodeNil(forKey: .name) { // 是否为nil
                self.name = ""
            } else {
                do {
                    // 是否类型正确
                    self.name = try container.decode(String.self, forKey: .name)
                } catch {
                    self.name = ""
                }
            }
        } else {
            self.name = ""
        }
    }
}

三. Encodable

swift 复制代码
 public protocol Encodable {
    func encode(to encoder: Encoder) throws
 }
swift 复制代码
 public protocol Encoder {
 ​
    var codingPath: [CodingKey] { get }
 ​
    var userInfo: [CodingUserInfoKey : Any] { get }
 ​
    func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
 ​
    func unkeyedContainer() -> UnkeyedEncodingContainer
 ​
    func singleValueContainer() -> SingleValueEncodingContainer
 }

Encodable协议相对Decodable协议较为简单,请参考Decodable篇。

1. EncodingError

编码失败,就会抛出异常,此时的error就是EncodingError。

java 复制代码
 public enum EncodingError : Error {
    case invalidValue(Any, EncodingError.Context)
 }

此Error就只有一种情况: 表示要编码的值无效或不符合预期的格式要求。

2. 三种编码容器

关于三种编码容器SingleValueDecodingContainerUnkeyedDecodingContainerKeyedDecodingContainer , 请参考 Decodable 的介绍。

3.编码时的 Optional值

Person模型中的name属性是可选值,并设置为了nil。进行encode的时候,不会以 null 写入json中。

swift 复制代码
 struct Person: Encodable {
    let name: String?
     
    init() {
        name = nil
    }
 }
 let encoder = JSONEncoder()
 guard let jsonData = try? encoder.encode(Person()) else { return }
 guard let json = String(data: jsonData, encoding: .utf8) else { return }
 print(json)
 // 输出: {}

这是因为系统进行encode的时候,使用的是 encodeIfPresent, 该方法不会对nil进行encode。等同于:

swift 复制代码
 struct Person: Encodable {
    let name: String?
     
    init() {
        name = nil
    }
     
    enum CodingKeys: CodingKey {
        case name
    }
     
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(self.name, forKey: .name)
 //       try container.encode(name, forKey: .name)
    }
 }

如果需要将这种情况仍然要写入json中,可以使用 try container.encode(name, forKey: .name) 。输出信息为: {"name":null}

4. encode 和 encodeIfPresent

如果不重写 func encode(to encoder: Encoder) throws 系统会根据encode属性是否可选类型决定使用哪个方法。

  • 可选属性:默认使用encodeIfPresent方法
  • 非可选属性:默认使用encode方法

四. userInfo

userInfo是一个 [CodingUserInfoKey : Any] 类型的字典,用于存储与解码过程相关的任意附加信息。它可以用来传递自定义的上下文数据或配置选项给解码器。可以在初始化Decoder时设置userInfo属性,并在解码过程中使用它来访问所需的信息。这对于在解码过程中需要使用额外的数据或配置选项时非常有用。

以下是一些使用userInfo属性的示例场景:

  1. 自定义解码行为:可以使用userInfo属性传递自定义的解码选项或配置给Decoder。例如,您可以在userInfo中设置一个布尔值,以指示解码器在遇到特定的键时执行特殊的解码逻辑。
  2. 传递上下文信息:如果需要在解码过程中访问一些上下文信息,例如用户身份验证令牌或当前语言环境设置。
  3. 错误处理:可以在userInfo中存储有关错误处理的信息。例如可以将一个自定义的错误处理器存储在userInfo中,以便在解码过程中捕获和处理特定类型的错误。
swift 复制代码
 struct Feed: Codable {
    let name: String
    let age: Int
     
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
         
        // 从userInfo中获取上下文信息
        if let language = decoder.userInfo[.init(rawValue: "language")!] as? String,
            let version = decoder.userInfo[.init(rawValue: "version")!] as? Double {
            print("Decoded using (language) (version)")
            // Decoded using Swift 5.0
        }
    }
 }
 ​
 let jsonData = """
 {
    "name": "John Doe",
    "age": 30
 }
 """.data(using: .utf8)!
 ​
 let decoder = JSONDecoder()
 // 设置userInfo属性,传递自定义的上下文信息
 let userInfo: [CodingUserInfoKey: Any] = [
    .init(rawValue: "language")!: "Swift",
    .init(rawValue: "version")!: 5.0]
 decoder.userInfo = userInfo
 guard let feed = try? decoder.decode(Feed.self, from: jsonData) else { return }
 print(feed)

五. CodingKey

在Swift中,CodingKey 是一个协议,用于在编码和解码过程中映射属性的名称。它允许 自定义属性名称编码键或解码键 之间的映射关系。CodingKey是Codable协议的核心之一,尤其是处理复杂结构的数据,发挥着至关重要的作用。

当使用 Codable 来编码和解码自定义类型时,Swift会自动合成编码和解码的实现。

  • 对于编码,Swift会将类型的属性名称作为键,将属性的值编码为相应的数据格式。
  • 对于解码,Swift会使用键来匹配编码的数据,并将数据解码为相应的属性值。
swift 复制代码
 public protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible, Sendable {
    var stringValue: String { get }
    init?(stringValue: String)
    var intValue: Int? { get }
    init?(intValue: Int)
 }

CodingKey协议要求实现一个名为 stringValue 的属性,用于表示键的字符串值。此外,还可以选择实现一个名为 intValue 的属性,用于表示键的整数值(用于处理有序容器,如数组)。

通过实现 CodingKey 协议,可以在编码和解码过程中使用自定义的键,以便更好地控制属性和编码键之间的映射关系。这对于处理不匹配的属性名称或与外部数据源进行交互时特别有用。

1. 处理不匹配的属性名称

有时候属性的名称和编码的键不完全匹配,或者希望使用不同的键来编码和解码属性。这时,可以通过实现 CodingKey 协议来自定义键。

使用CodingKey将 nick_name 字段重命名为 name

python 复制代码
 struct Feed: Codable {
    var name: String
    var id: Int = 0
     
    private enum CodingKeys: String, CodingKey {
        case name = "nick_name"
        // 只实现声明的属性的映射。
        // case id
    }
 }
 ​
 let json = """
 {
    "nick_name": "xiaoming",
    "id": 10
 }
 """
 guard let feed = json.decode(type: Feed.self) else { return }
 print(feed)
 // Feed(name: "xiaoming", id: 0)

通过实现CodingKey协议,我们成功的将不匹配的属性名称(nick_name) 完成了映射。

  • CodingKeys只会处理实现的映射关系,没实现的映射关系(比如: id),将不会解析。
  • CodingKeys最好使用private修饰,避免被派生类继承。
  • CodingKeys必须是嵌套在声明的struct中的。

2. 扁平化解析

我们可以用枚举来实现CodingKey,用来处理不匹配的属性映射关系。还可以使用结构体来实现,提供更灵活的使用,处理复杂的层级结构。

bash 复制代码
 let res = """
 {
    "player_name": "balabala Team",
    "age": 20,
    "native_Place": "shandong",
    "scoreInfo": {
        "gross_score": 2.4,
        "scores": [
            0.9,
            0.8,
            0.7
        ],
        "remarks": {
            "judgeOne": {
                "content": "good"
            },
            "judgeTwo": {
                "content": "very good"
            },
            "judgeThree": {
                "content": "bad"
            }
        }
    }
 }
 ""

提供这样一个较为复杂的json结构,期望将这个json扁平化的解析到Player结构体中。

vbnet 复制代码
 struct Player {
    let name: String
    let age: Int
    let nativePlace: String
    let grossScore: CGFloat
    let scores: [CGFloat]
    let remarks: [Remark]
 }
 struct Remark {
    var judge: String
    var content: String
 }
 ​
 /** 解析完成的结构是这样的
 Player(
        name: "balabala Team",
        age: 20,
        nativePlace: "shandong",
        grossScore: 2.4,
        scores: [0.9, 0.8, 0.7],
        remarks: [
                  Remark(judge: "judgeTwo", content: "very good"),
                  Remark(judge: "judgeOne", content: "good"),
                  Remark(judge: "judgeThree", content: "bad")
                ]
 )
 */

实现逻辑是这样的:

swift 复制代码
 struct Remark: Codable {
    var judge: String
    var content: String
 }
 ​
 struct Player: Codable {
    let name: String
    let age: Int
    let nativePlace: String
    let grossScore: CGFloat
    let scores: [CGFloat]
    let remarks: [Remark]
     
    // 当前容器中需要包含的key
    enum CodingKeys: String, CodingKey {
        case name = "player_name"
        case age
        case nativePlace = "native_Place"
        case scoreInfo
    }
    
    enum ScoreInfoCodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
        case remarks
    }
     
    struct RemarkCodingKeys: CodingKey {
        var intValue: Int? {return nil}
        init?(intValue: Int) {return nil}
        var stringValue: String //json中的key
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        static let content = RemarkCodingKeys(stringValue: "content")!
    }
     
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
        self.nativePlace = try container.decode(String.self, forKey: .nativePlace)
         
        let scoresContainer = try container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
        self.grossScore = try scoresContainer.decode(CGFloat.self, forKey: .grossScore)
        self.scores = try scoresContainer.decode([CGFloat].self, forKey: .scores)
         
        let remarksContainer = try scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
         
        var remarks: [Remark] = []
        for key in remarksContainer.allKeys { //key的类型就是映射规则的类型(Codingkeys)
            let judge = key.stringValue
            print(key)
            /**
              RemarkCodingKeys(stringValue: "judgeTwo", intValue: nil)
              RemarkCodingKeys(stringValue: "judgeOne", intValue: nil)
              RemarkCodingKeys(stringValue: "judgeThree", intValue: nil)
              */
            let keyedContainer = try remarksContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: key)
            let content = try keyedContainer.decode(String.self, forKey: .content)
            let remark = Remark(judge: judge, content: content)
            remarks.append(remark)
        }
        self.remarks = remarks
    }
 ​
    func encode(to encoder: Encoder) throws {
        // 1. 生成最外层的字典容器
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(nativePlace, forKey: .nativePlace)
         
        // 2. 生成scoreInfo字典容器
        var scoresContainer = container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
        try scoresContainer.encode(grossScore, forKey: .grossScore)
        try scoresContainer.encode(scores, forKey: .scores)
         
        // 3. 生成remarks字典容器(模型中结构是数组,但是我们要生成字典结构)
        var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
        for remark in remarks {
            var remarkContainer = remarksContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: RemarkCodingKeys(stringValue: remark.judge)!)
            try remarkContainer.encode(remark.content, forKey: .content)
        }
    }
 }
1. 解码最外层数据

层级结构:包含4个key的字典结构。

json 复制代码
 {
    "player_name": ...
    "age": ...
    "native_Place": ...
    "scoreInfo": ...    
 }
  • 生成容器

    根据这个结构,使用这个CodingKeys实现数据和模型之间的映射关系。

    php 复制代码
     enum CodingKeys: String, CodingKey {
        case name = "player_name"
        case age
        case nativePlace = "native_Place"
        case scoreInfo
     }
     ​
     var container = encoder.container(keyedBy: CodingKeys.self)
  • 容器解码

    php 复制代码
     try container.encode(name, forKey: .name)
     try container.encode(age, forKey: .age)
     try container.encode(nativePlace, forKey: .nativePlace)
     ​
     // scoreInfo是下一层的数据
2. 解码scoreInfo层数据

层级结构:包含3个key的字典结构。

json 复制代码
 {
    "gross_score": ...
    "scores": ...
    "remarks": ...
 }
  • 生成容器

    根据这个结构,我们使用这个ScoreInfoCodingKeys实现本层的数据和模型之间的映射关系。

    php 复制代码
     enum ScoreInfoCodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
        case remarks
     }
     var scoresContainer = container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
  • 容器解码

    php 复制代码
     try scoresContainer.encode(grossScore, forKey: .grossScore)
     try scoresContainer.encode(scores, forKey: .scores)
     ​
     // remarks层是下层的数据
3. 解码remarks层数据

我们期望进行这样的层级转换

  • 生成容器

    swift 复制代码
     struct RemarkCodingKeys: CodingKey {
        var intValue: Int? {return nil}
        init?(intValue: Int) {return nil}
        var stringValue: String //json中的key
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        static let content = RemarkCodingKeys(stringValue: "content")!
     }
     var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)

    使用scoresContainer容器生成一个新的remarksContainer容器,表示remarksContainer容器是在scoresContainer容器内。

  • 容器解码

    php 复制代码
     for remark in remarks {
        var remarkContainer = remarksContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: RemarkCodingKeys(stringValue: remark.judge)!)
        try remarkContainer.encode(remark.content, forKey: .content)
     }

    remarks即模型中的[Remark]类型的属性。遍历remarks,使用remarksContainer生成一个新的容器remarkContainer。

    该容器使用RemarkCodingKeys作为映射关系,使用remark的judge生成的CodingKey作为Key。 即:

    less 复制代码
     RemarkCodingKeys(stringValue: "judgeTwo", intValue: nil)
     RemarkCodingKeys(stringValue: "judgeOne", intValue: nil)
     RemarkCodingKeys(stringValue: "judgeThree", intValue: nil)

    最后将数据填充到remarkContainer里面: try remarkContainer.encode(remark.content, forKey: .content)

4. 总结
  • CodingKey即可以是枚举,又可以是结构体。

  • 通过CodingKey生成容器可以这么理解:CodingKey是一种映射关系,旨在生成这种映射关系的容器。此时的容器并不关系内容是什么。

    php 复制代码
     var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
  • 执行encode的时候,才是对该容器的内容填充,此时才用到CodingKey的内容。

    php 复制代码
     try remarkContainer.encode(remark.content, forKey: .content)

3. 深入理解CodingKey

encode 需要做这两件事(或不断重复这两个事)

  • 生成容器(创建层级结构)
  • 容器解码(填充对应数据)

我们来看一个案例,帮助理解这 "两件事":

swift 复制代码
 struct FeedOne: Codable {
    var id: Int = 100
    var name: String = "xiaoming"
     
    enum CodingKeys: CodingKey {
        case id
        case name
    }
     
    func encode(to encoder: Encoder) throws {
        var conrainer = encoder.container(keyedBy: CodingKeys.self)
    }
 }
 ​
 let feed = Feed()
 guard let value = feed.encode() else { return }
 print(value)
 ​
 //输出信息是一个空字典
 {
 ​
 }

如果我们使用Struct自定义CodingKey

swift 复制代码
 struct FeedOne: Codable {
    var id: Int = 100
    var name: String = "xiaoming"
     
    struct CustomKeys: CodingKey {
        var stringValue: String
         
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
         
        var intValue: Int?
         
        init?(intValue: Int) {
            self.stringValue = ""
        }
    }
     
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CustomKeys.self)
    }
 }
 ​
 //输出信息是一个空字典
 {
 ​
 }

经过这两个例子,应该更了解CodingKey。

CodingKey表示一种映射关系或一个规则,通过CodingKey生成的带有这种映射关系的容器。

只有向容器中填充时才在意内容,填充的信息包含两部分, key 和 value。

swift 复制代码
 func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
     
    try container.encode(id, forKey: .id)
    try container.encode(name, forKey: .name)
 }
swift 复制代码
 func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CustomKeys.self)
     
    try container.encode(id, forKey: .init(stringValue: "id")!)
    try container.encode(name, forKey: .init(stringValue: "name")!)
 }

无论是enum的CodingKey还是自定义的结构体CodingKey,此时的value都输出为:

json 复制代码
 {
  "id" : 100,
  "name" : "xiaoming"
 }

4. CodingKey的可选设计

CodingKeys的初始化方法被设计成可选的,是为了处理可能存在的键名不匹配的情况。

当我们从外部数据源(如JSON)中解码数据时,属性名与键名必须一致才能正确地进行解码操作。但是,外部数据源的键名可能与我们的属性名不完全匹配,或者某些键可能在数据源中不存在。通过将CodingKeys的初始化方法设计为可选的,我们可以在解码过程中处理这些不匹配的情况。

如果某个键在数据源中不存在,我们可以将其设置为nil,或者使用默认值来填充属性。这样可以确保解码过程的稳定性,并避免由于键名不匹配而导致的解码错误。

六. codingPath

在Swift中,Codable协议用于将自定义类型与外部数据进行编码和解码。codingPathCodable协议中的一个属性,它表示当前正在编码或解码的属性的路径。

codingPath是一个数组,它按照嵌套层次结构记录了属性的路径。每个元素都是一个CodingKey类型的值,它表示当前层级的属性名称。

通过检查codingPath,您可以了解正在处理的属性的位置,这对于处理嵌套的数据结构非常有用。

相关推荐
问道飞鱼1 小时前
【移动端知识】移动端多 WebView 互访方案:Android、iOS 与鸿蒙实现
android·ios·harmonyos·多webview互访
mascon2 小时前
U3D打包IOS的自我总结
ios
名字不要太长 像我这样就好2 小时前
【iOS】继承链
macos·ios·cocoa
karshey3 小时前
【IOS webview】IOS13不支持svelte 样式嵌套
ios
潜龙95273 小时前
第4.3节 iOS App生成追溯关系
macos·ios·cocoa
游戏开发爱好者812 小时前
iOS App 电池消耗管理与优化 提升用户体验的完整指南
android·ios·小程序·https·uni-app·iphone·webview
神策技术社区19 小时前
iOS 全埋点点击事件采集白皮书
大数据·ios·app
wuyoula20 小时前
iOS V2签名网站系统源码/IPA在线签名/全开源版本/亲测
ios
2501_9159184120 小时前
iOS 性能监控工具全解析 选择合适的调试方案提升 App 性能
android·ios·小程序·https·uni-app·iphone·webview
fishycx20 小时前
iOS 构建配置与 AdHoc 打包说明
ios