Swift 網(wǎng)絡數(shù)據(jù)模型解析

引言

在項目中網(wǎng)絡數(shù)據(jù)解析是最普通的功能了。最常見的就是JSON數(shù)據(jù)。在項目最開始我們使用了Argo 進行模型解析。Argo+Curry+Runes完成網(wǎng)絡數(shù)據(jù)轉(zhuǎn)Model。但隨著蘋果發(fā)布swift 4.0 引入了 JSONDecoder、JSONEncoder,加上Argo 也不再維護。所以我們準備重新設計數(shù)據(jù)模型解析。
這里我就不介紹JSONDecoder、Codable相關內(nèi)容了。不了解的可以先自行百度。這次只分享在實現(xiàn)過程中遇到的坑及解決方法。
網(wǎng)絡請求是一個不穩(wěn)定的過程,接口數(shù)據(jù)不可能和理想數(shù)據(jù)完全一樣。你可能會遇到某一個字段找不到;本來是Int,結(jié)果是String(這點在php 后臺非常明顯);本來是String,結(jié)果來了個Int 。Swift 是一個類型嚴格的語言。只要類型不一樣,就隨時可能crash。所以要開發(fā)模型解析庫,第一個要解決的問題就是類型安全問題。參考其他網(wǎng)絡模型解析(出名的有OC 的YYModelMJExtension,swift有之前使用的Argo)。在數(shù)據(jù)解析過程中如果接口數(shù)據(jù)不是目標類型,那么會做一次類型轉(zhuǎn)換。那么在Swift 中是否也可以這樣做那。先來看下Swift 的類型轉(zhuǎn)換方法吧!
鍵值對的解析 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<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T : Decodable

這里可以看到解析時要先傳入類型和相對應的key。我嘗試過。這種解析只會給什么類型就解析什么。不能達到我們前面提到的目的。所以只有自己動手才能豐衣足食啊。


流淚

首先來分析下原生解析。對于絕大多數(shù)情況,一般我們定義什么類型就期望解析成什么類型。比如

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 參數(shù)其實可以去掉。因為Swift 是支持泛型的。理想情況是我告訴container 我要解析.name 。那么它應該能夠推斷出String 類型。
那么我們最終的處理結(jié)果是

  1. 要對數(shù)據(jù)類型容錯
  2. 避免重復類型的聲明,讓編譯器自動推導

最終我封裝成了下面的代碼

// MARK: - 鍵值對解碼
extension KeyedDecodingContainer {
 func decode<T: Decodable>(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!
    }
}

篇幅有限不全貼代碼了。思路就是對所有基本類型做封裝,如果直接轉(zhuǎn)換失敗,遇到了typeMismatch 錯誤。那么再次嘗試其他類型轉(zhuǎn)換。比如常見的String -> Int
String->Float, String -> Double, Int > Bool ,String -> Bool等。
所以現(xiàn)在的模型解析就可以簡化成這樣了。

 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<T: Decodable>() 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
    }
}

數(shù)組解析

extension KeyedDecodingContainer {
 //MARK: collection解碼
    /* 序列編碼 目前只自定義了Array  */
    func sequenceDecode<T: Collection & Decodable>(key: KeyedDecodingContainer.Key) throws -> T where T.Element: Decodable {
        var values: T
        if T.self == Array<T.Element>.self {
            var array = Array<T.Element>.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解析失敗導致整個數(shù)組解析都失敗。
在實際開發(fā)中有些字段并不是must 的,所以一些optional 字段是可以解析失敗的。于是又擴展了對 optional 字段解析

 /* 解碼optional 的 不會轉(zhuǎn)碼失敗 */
    func decodeIfPresent<T: Decodable>(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!!
    }

這樣既可以保證數(shù)據(jù)的正確性又有很強的容錯性。

struct LifeStyleItem: JSONDecodable {
    let name: String //生活指數(shù)名字
    let icon: String
    let desc: String? //生活指數(shù)描述
    let suggest: String? //建議,目前服務器暫時不傳
    let color: String //取值范圍:"red", "green",red表示超標,green在安全值范圍內(nèi)
    let type: LifeStyleType  // "1"表示原生指數(shù),"2"表示運營指數(shù),等等(穿衣指數(shù)屬于原生指數(shù)),目前服務器暫時不傳,客戶端保留,默認值為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)
    }
}

整個解析差不多已經(jīng)OK?,F(xiàn)在還差最后一點。與網(wǎng)絡接口對接。
于是定義了以下協(xié)議:

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 并實現(xiàn) Decodable 、Encodable 協(xié)議就可以完成模型解析。

最后解析可以簡化為

let festival = Festival.decode(json)
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容