引言
在項目中網(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 的YYModel、 MJExtension,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é)果是
- 要對數(shù)據(jù)類型容錯
- 避免重復類型的聲明,讓編譯器自動推導
最終我封裝成了下面的代碼
// 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)