Swift是时候使用Codable了

语言: CN / TW / HK

用不起:

苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了,

比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子直接掀桌子,整个模型为nil。这。。。

而且模型的属性想要默认值,无。。。

image.png

你牛,牛到大家不知道怎么用

于是网络一边夸他Codable好用,一边真正工程开发中却还用不起来。

搞起来:

最近研究网上有没有好用的Codable库的时候,找到了这个。2021 年了,Swift 的 JSON-Model 转换还能有什么新花样https://github.com/iwill/ExCodable

经过他的封装,把苹果包装的服服帖帖。经测试,解决如下问题:

  1. 多一个key
  2. 少一个key
  3. key的类型不匹配的时候,自动做类型转换
  4. 默认值处理好。 

image.png

他的模型定义可以简化为: struct testModel: ExAutoCodable { @ExCodable var courseId: Int = -1 @ExCodable var totalSectionCount: Int = -1 // 总的章节 @ExCodable var courseImageUrl: String = "" @ExCodable var tudiedSectionCount: Int = 0 // 已经学习章节 }

既然他这么好,那就用起来啰喂,,,,等等,等等

定义模型这样,竟然不行:

``` struct testModel: ExAutoCodable { @ExCodable var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []

} ```

苹果老子说Any不支持Codable???转模型的时候,这个全是空,nil。

一看工程,基本每个模型的定义都有这个呀,全有Any的定义,懵逼

研究起来:

通过研究https://stackoverflow.com/questions/48297263/how-to-use-any-in-codable-type, 发现可以给Any封装一个支持Codable的类型,比如AnyCodable这样。然后模型里面用到Any的,全部给换成AnyCodable。

image.png

模型改为如下,使用AnyCodable

``` struct testModel: ExAutoCodable { @ExCodable var jumpParam: [String: AnyCodable]? = [:]

@ExCodable
var matchs: [AnyCodable] = []

} ```

AnyCodable.swift代码如下。 ``` // // AnyCodable.swift // // 因为Any不支持Codable,但是模型里面经常会用到[String: Any]。 // 所以添加类AnyCodable,代替Any,来支持Codable, 如:[String: AnyCodable]。 // https://stackoverflow.com/questions/48297263/how-to-use-any-in-codable-type

import Foundation

public struct AnyCodable: Decodable { var value: Any

struct CodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?
    init?(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
    init?(stringValue: String) { self.stringValue = stringValue }
}

init(value: Any) {
    self.value = value
}

public init(from decoder: Decoder) throws {
    if let container = try? decoder.container(keyedBy: CodingKeys.self) {
        var result = [String: Any]()
        try container.allKeys.forEach { (key) throws in
            result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
        }
        value = result
    } else if var container = try? decoder.unkeyedContainer() {
        var result = [Any]()
        while !container.isAtEnd {
            result.append(try container.decode(AnyCodable.self).value)
        }
        value = result
    } else if let container = try? decoder.singleValueContainer() {
        if let intVal = try? container.decode(Int.self) {
            value = intVal
        } else if let doubleVal = try? container.decode(Double.self) {
            value = doubleVal
        } else if let boolVal = try? container.decode(Bool.self) {
            value = boolVal
        } else if let stringVal = try? container.decode(String.self) {
            value = stringVal
        } else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
        }
    } else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
    }
}

}

extension AnyCodable: Encodable { public func encode(to encoder: Encoder) throws { if let array = value as? [Any] { var container = encoder.unkeyedContainer() for value in array { let decodable = AnyCodable(value: value) try container.encode(decodable) } } else if let dictionary = value as? [String: Any] { var container = encoder.container(keyedBy: CodingKeys.self) for (key, value) in dictionary { let codingKey = CodingKeys(stringValue: key)! let decodable = AnyCodable(value: value) try container.encode(decodable, forKey: codingKey) } } else { var container = encoder.singleValueContainer() if let intVal = value as? Int { try container.encode(intVal) } else if let doubleVal = value as? Double { try container.encode(doubleVal) } else if let boolVal = value as? Bool { try container.encode(boolVal) } else if let stringVal = value as? String { try container.encode(stringVal) } else { throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable")) } } } } ```

这个结合Excodable,经过测试,完美。数据转换成功。

如果模型的定义忘记了,还是定义为Any呢。 再给Excodable库里面的源码,做安全检查,修改代码如下:

``` public extension Encodable { func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { for child in mirror.children where child.label != nil { try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) // 注意:Any不支持Codable, 可以使用AnyCodable代替。 // 注意枚举类型,要支持Codable assert((child.value as? EncodablePropertyWrapper) != nil, "模型:(mirror)里面的属性:(child.label) 需要支持 Encodable") } mirror = mirror.superclassMirror } } }

public extension Decodable { func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { for child in mirror.children where child.label != nil { try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false) // 注意:Any不支持Codable, 可以使用AnyCodable代替。 // 注意枚举类型,要支持Codable assert((child.value as? DecodablePropertyWrapper) != nil, "模型:(mirror)里面的属性:(child.label) 需要支持 Decodable") } mirror = mirror.superclassMirror } } } ```

嗯,这下模型如果定义为Any,可以在运行的时候报错,提醒要改为AnyCodable。

能愉快的编码了。。。

不过总感觉还差点东西。

再研究起来:

找到这个 https://github.com/levantAJ/AnyCodable

可以实现

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key) let array: [Any] = try container.decode([Any].self, forKey: key)

通过自定义[String: Any]和[Any]的解码,实现Any的Codble。

是否可以把这个合并到Excodable里面吧,从而什么都支持了,666。

在Excodable里面提issues,作者回复有空可以弄弄。

我急用呀,那就搞起来。

花了九牛二虎,终于搞出下面兼容代码。

// Make `Any` support Codable, like: [String: Any], [Any] fileprivate protocol EncodableAnyPropertyWrapper { func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws } extension ExCodable: EncodableAnyPropertyWrapper { fileprivate func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws { if encode != nil { try encode!(encoder, wrappedValue) } else { let t = type(of: wrappedValue) if let key = AnyCodingKey(stringValue: String(label)) { if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) { var container = try encoder.container(keyedBy: AnyCodingKey.self) try container.encodeIfPresent(wrappedValue as? [String: Any], forKey: key) } else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) { var container = try encoder.container(keyedBy: AnyCodingKey.self) try container.encodeIfPresent(wrappedValue as? [Any], forKey: key) } } } } } fileprivate protocol DecodableAnyPropertyWrapper { func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws } extension ExCodable: DecodableAnyPropertyWrapper { fileprivate func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws { if let decode = decode { if let value = try decode(decoder) { wrappedValue = value } } else { let t = type(of: wrappedValue) if let key = AnyCodingKey(stringValue: String(label)) { if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) { let container = try decoder.container(keyedBy: AnyCodingKey.self) if let value = try container.decodeIfPresent([String: Any].self, forKey: key) as? Value { wrappedValue = value } } else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) { let container = try decoder.container(keyedBy: AnyCodingKey.self) if let value = try container.decodeIfPresent([Any].self, forKey: key) as? Value { wrappedValue = value } } } } } }

再在他用的地方添加

``` // MARK: - Encodable & Decodable - internal

public extension Encodable { func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { for child in mirror.children where child.label != nil { if let wrapper = (child.value as? EncodablePropertyWrapper) { try wrapper.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) } else { //添加 try (child.value as? EncodableAnyPropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) } } mirror = mirror.superclassMirror } } }

public extension Decodable { func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { for child in mirror.children where child.label != nil { if let wrapper = (child.value as? DecodablePropertyWrapper) { try wrapper.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false) } else { //添加 try (child.value as? DecodableAnyPropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false) } } mirror = mirror.superclassMirror } } }

```

完美:

综上,终于可以让Excodable库支持[String: Any]和[Any]的Codable了,撒花撒花。

从而模型定义这样,也能自动编解码。

``` struct testModel: ExAutoCodable { @ExCodable var jumpParam: [String: Any]? = [:]

@ExCodable var matchs: [Any] = [] } ```

针对这个库的更新修改,改到这https://github.com/yxh265/ExCodable/tree/develop

也把对应的更新提交给Excodable的作者了,期待合并。 (作者iwill说,用ExCodable提供的 ExCodableDecodingTypeConverter 协议来实现是否可行。 我看了,因为Any不支持Codable,所以要想用ExCodableDecodingTypeConverter协议,也得要大改。也期待作者出马添加这个功能。)

最后的使用方法:

引入如下: pod 'ExCodable', :git => 'https://github.com/yxh265/ExCodable.git', :commit => '4780fb8'

模型定义: struct TestStruct: ExAutoCodable { @ExCodable // 字段和属性同名可以省掉字段名和括号,但 `@ExCodable` 还是没办法省掉 var int: Int = 0 @ExCodable("string", "str", "s", "nested.string") // 支持多个 key 以及嵌套 key 可以这样写 var string: String? = nil @ExCodable var anyDict: [String: Any]? = nil @ExCodable var anyArray: [Any] = [] } 编解码: let test = TestStruct(int: 304, string: "Not Modified", anyDict: ["1": 2, "3": "4"], anyArray: [["1": 2, "3": "4"]]) let data = try? test.encoded() as Data? let copy1 = try? data?.decoded() as TestStruct? let copy2 = data.map { try? TestStruct.decoded(from: $0) } XCTAssertEqual(copy1, test) XCTAssertEqual(copy2, test)

引用:

2021 年了,Swift 的 JSON-Model 转换还能有什么新花样

https://github.com/iwill/ExCodable

https://stackoverflow.com/questions/44603248/how-to-decode-a-property-with-type-of-json-dictionary-in-swift-45-decodable-pr/46383107#46383107

https://stackoverflow.com/questions/48297263/how-to-use-any-in-codable-type

Property wrappers in Swift和Codable