Swift 网络数据模型解析

引言

在项目中网络数据解析是最普通的功能了。最常见的就是JSON数据。在项目最开始我们使用了Argo 进行模型解析。Argo+Curry+Runes完成网络数据转Model。但随着苹果发布swift 4.0 引入了 JSONDecoder、JSONEncoder,加上Argo 也不再维护。所以我们准备重新设计数据模型解析。
这里我就不介绍JSONDecoderCodable相关内容了。不了解的可以先自行百度。这次只分享在实现过程中遇到的坑及解决方法。
网络请求是一个不稳定的过程,接口数据不可能和理想数据完全一样。你可能会遇到某一个字段找不到;本来是Int,结果是String(这点在php 后台非常明显);本来是String,结果来了个Int 。Swift 是一个类型严格的语言。只要类型不一样,就随时可能crash。所以要开发模型解析库,第一个要解决的问题就是类型安全问题。参考其他网络模型解析(出名的有OC 的YYModelMJExtension,swift有之前使用的Argo)。在数据解析过程中如果接口数据不是目标类型,那么会做一次类型转换。那么在Swift 中是否也可以这样做那。先来看下Swift 的类型转换方法吧!
键值对的解析 KeyedDecodingContainer

public func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer.Key) throws -> Bool
public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String
public func decode(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T : Decodable

这里可以看到解析时要先传入类型和相对应的key。我尝试过。这种解析只会给什么类型就解析什么。不能达到我们前面提到的目的。所以只有自己动手才能丰衣足食啊。


流泪

首先来分析下原生解析。对于绝大多数情况,一般我们定义什么类型就期望解析成什么类型。比如

struct Person: Decodable {
    let name: String
    let age: Int
    
    enum PersonCodingKey: String, CodingKey {
        case name
        case age
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: PersonCodingKey.self)
        name = try container.decode(String.self, forKey: .name)
        age = try container.decode(Int.self, forKey: .age)
    }
}

但这里String.self 参数其实可以去掉。因为Swift 是支持泛型的。理想情况是我告诉container 我要解析.name 。那么它应该能够推断出String 类型。
那么我们最终的处理结果是

  1. 要对数据类型容错
  2. 避免重复类型的声明,让编译器自动推导

最终我封装成了下面的代码

// MARK: - 键值对解码
extension KeyedDecodingContainer {
 func decode(key: KeyedDecodingContainer.Key) throws -> T {
        var value: T
        if T.self == Int.self {
            value = try _safeDecode(Int.self, forKey: key) as! T
        }else if T.self == Int8.self {
            value = try _safeDecode(Int8.self, forKey: key) as! T
        }else if T.self == Int16.self {
            value = try _safeDecode(Int16.self, forKey: key) as! T
        }else if T.self == Int32.self {
            value = try _safeDecode(Int32.self, forKey: key) as! T
        }else if T.self == Int64.self {
            value = try _safeDecode(Int64.self, forKey: key) as! T
        }else if T.self == UInt8.self {
            value = try _safeDecode(UInt8.self, forKey: key) as! T
        }else if T.self == UInt16.self {
            value = try _safeDecode(UInt16.self, forKey: key) as! T
        }else if T.self == UInt32.self {
            value = try _safeDecode(UInt32.self, forKey: key) as! T
        }else if T.self == UInt64.self {
            value = try _safeDecode(UInt64.self, forKey: key) as! T
        }else if T.self == String.self {
            value = _safeDecode(String.self, forKey: key) as! T
        }else if T.self == Bool.self {
            value = try _safeDecode(Bool.self, forKey: key) as! T
        }else if T.self == Float.self {
            value = try _safeDecode(Float.self, forKey: key) as! T
        }else if T.self == Double.self {
            value = try _safeDecode(Double.self, forKey: key) as! T
        }else {
            value = try decode(T.self, forKey: key)
        }
        return value
    }
  private func _safeDecode(_ type: Int.Type, forKey key: KeyedDecodingContainer.Key) throws -> Int  {
        var value: Int?
        do {
            value = try decode( Int.self, forKey: key)
        } catch {
            let err = error as! DecodingError
            switch err {
            case .typeMismatch(_, _):
                value = try (decodeIfPresent(String.self, forKey: key) as NSString?)?.integerValue
                if value == nil {
                    throw err
                }
            default:
                throw err
            }
        }
        return value!
    }
}

篇幅有限不全贴代码了。思路就是对所有基本类型做封装,如果直接转换失败,遇到了typeMismatch 错误。那么再次尝试其他类型转换。比如常见的String -> Int
String->Float, String -> Double, Int > Bool ,String -> Bool等。
所以现在的模型解析就可以简化成这样了。

 public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: PersonCodingKey.self)
        name = try container.decode(key: .name)
        age = try container.decode(key: .age)
    }

这样解析语义也更加清晰。
按照这个思路对其他进行扩展

extension SingleValueDecodingContainer {
    func decode() throws -> T {
        var value: T
        if T.self == Int.self {
            value = try _safedecode(Int.self) as! T
        }else if T.self == String.self {
            value = try _safedecode(String.self) as! T
        } else if T.self == Float.self {
            value = try _safedecode(Float.self) as! T
        }else {
            value = try decode(T.self)
        }
        return value
    }
}

数组解析

extension KeyedDecodingContainer {
 //MARK: collection解码
    /* 序列编码 目前只自定义了Array  */
    func sequenceDecode(key: KeyedDecodingContainer.Key) throws -> T where T.Element: Decodable {
        var values: T
        if T.self == Array.self {
            var array = Array.init()
            var unkeyed = try nestedUnkeyedContainer(forKey: key)
            while !unkeyed.isAtEnd {
                var subValue: T.Element? = nil
                if T.Element.self == Int.self {
                    subValue = decode(Int.self, unkeyed: &unkeyed) as? T.Element
                }else if T.Element.self == String.self {
                    subValue = decode(String.self, unkeyed: &unkeyed) as? T.Element
                }else if T.Element.self == Float.self {
                    subValue = decode(Float.self, unkeyed: &unkeyed) as? T.Element
                }else {
                    subValue = try unkeyed.decode(T.Element.self)
                }
                if subValue != nil {
                    array.append(subValue!)
                }
            }
            values = array as! T
        }else{
            values = try decode(T.self, forKey: key)
        }
        return values
    }
}

我只对基本类型进行了扩展。防止某一个item解析失败导致整个数组解析都失败。
在实际开发中有些字段并不是must 的,所以一些optional 字段是可以解析失败的。于是又扩展了对 optional 字段解析

 /* 解码optional 的 不会转码失败 */
    func decodeIfPresent(key: KeyedDecodingContainer.Key) -> T? {
        var value: T??
        if T.self == Int.self {
            value = try? _safeDecode(Int.self, forKey: key) as? T
        }else if T.self == Int8.self {
            value = try? _safeDecode(Int8.self, forKey: key) as? T
        }else if T.self == Int16.self {
            value = try? _safeDecode(Int16.self, forKey: key) as? T
        }else if T.self == Int32.self {
            value = try? _safeDecode(Int32.self, forKey: key) as? T
        }else if T.self == Int64.self {
            value = try? _safeDecode(Int64.self, forKey: key) as? T
        }else if T.self == UInt8.self {
            value = try? _safeDecode(UInt8.self, forKey: key) as? T
        }else if T.self == UInt16.self {
            value = try? _safeDecode(UInt16.self, forKey: key) as? T
        }else if T.self == UInt32.self {
            value = try? _safeDecode(UInt32.self, forKey: key) as? T
        }else if T.self == UInt64.self {
            value = try? _safeDecode(UInt64.self, forKey: key) as? T
        }else if T.self == String.self {
            value = _safeDecode(String.self, forKey: key) as? T
        }else if T.self == Bool.self {
            value = try? _safeDecode(Bool.self, forKey: key) as? T
        }else if T.self == Float.self {
            value = try? _safeDecode(Float.self, forKey: key) as? T
        }else if T.self == Double.self {
            value = try? _safeDecode(Double.self, forKey: key) as? T
        }else {
            value = try? decode(T.self, forKey: key) as T?
        }
        if value == nil {
            return nil
        }
        return value!!
    }

这样既可以保证数据的正确性又有很强的容错性。

struct LifeStyleItem: JSONDecodable {
    let name: String //生活指数名字
    let icon: String
    let desc: String? //生活指数描述
    let suggest: String? //建议,目前服务器暂时不传
    let color: String //取值范围:"red", "green",red表示超标,green在安全值范围内
    let type: LifeStyleType  // "1"表示原生指数,"2"表示运营指数,等等(穿衣指数属于原生指数),目前服务器暂时不传,客户端保留,默认值为1
}
extension LifeStyleItem {
    private enum LifeStyleItemCodingKey: String, CodingKey {
        case name
        case icon
        case desc
        case suggest
        case color
        case type
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: LifeStyleItemCodingKey.self)
        name = try container.decode(key: .name)
        icon = try container.decode(key: .icon)
        desc = container.decodeIfPresent(key: .desc)
        suggest = container.decodeIfPresent(key: .suggest)
        color = try container.decode(key: .color)
        type = try container.decode(key: .type)
    }
}

整个解析差不多已经OK。现在还差最后一点。与网络接口对接。
于是定义了以下协议:

protocol JSONDecodable: Decodable {
    static func decode(_ json: [String: Any]?) -> Self?
    static func decode(_ json: [[String: Any]?]?) -> [Self]
}
extension JSONDecodable {
    static func decode(_ json: [String: Any]?) -> Self? {
        guard json != nil else {
            return nil
        }
        let data = try? JSONSerialization.data(withJSONObject: json!, options: JSONSerialization.WritingOptions(rawValue: 0))
        guard data != nil else {
            return nil
        }
        let decode = JSONDecoder.init()
        var model: Self? = nil
        do {
            model = try decode.decode(Self.self, from: data!)
        } catch {
            print("decode \(Self.self) error \(error)")
        }
        return model
    }
    static func decode(_ json: [[String: Any]?]?) -> [Self] {
        guard json != nil else {
            return []
        }
        var models: [Self] = []
        let decode = JSONDecoder.init()
        json!.forEach({ (jsonData: [String: Any]?) in
            if jsonData == nil {
                return
            }
            let data = try? JSONSerialization.data(withJSONObject: jsonData!, options: JSONSerialization.WritingOptions(rawValue: 0))
            if data == nil {
                return
            }
            var model: Self?
            do{
                model = try decode.decode(Self.self, from: data!)
            }catch{
                return
            }
            models.append(model!)
        })
        return models
    }
}
protocol JSONEncodable: Encodable {
    func encode() -> [String: Any]?
}
extension JSONEncodable {
    func encode() -> [String: Any]? {
        let encoder = JSONEncoder()
        let codedValue = try? encoder.encode(self)
        guard let codeData = codedValue else {
            return nil
        }
        let json = try? JSONSerialization.jsonObject(with: codeData, options: JSONSerialization.ReadingOptions(rawValue: 0))
        return json as? [String: Any]
    }
}
typealias JSONCodable = JSONDecodable & JSONEncodable

这样模型就遵循 JSONCodable 并实现 DecodableEncodable 协议就可以完成模型解析。

最后解析可以简化为

let festival = Festival.decode(json)

你可能感兴趣的:(Swift 网络数据模型解析)