Swift4中Codable的使用(二)

本篇是Swift4中Codable的使用系列第二篇,繼上一篇文章,我們學習了Codable協議在json與模型之間編碼和解碼的基本使用。本篇我們將了解Codable中,如何實現自定義模型轉json編碼和自定義json轉模型解碼的過程。


對于自定義模型轉json編碼和自定義json轉模型解碼的過程,我們只需要在該類型中重寫Codable協議中的編碼和解碼方法即可:

public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}
public protocol Decodable {
    public init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable

我們先定義一個Student模型來進行演示:

struct Student: Codable {
    let name: String
    let age: Int
    let bornIn: String
    
    // 映射規則,用來指定屬性和json中key兩者間的映射的規則
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case bornIn = "born_in"
    }
}

重寫系統的方法,實現與系統一樣的decode和encode效果

在自定義前,我們先來把這兩個方法重寫成系統默認的實現來了解一下,對于這兩個方法,我們要掌握的是container的用法。

    init(name: String, age: Int, bornIn: String) {
        self.name = name
        self.age = age
        self.bornIn = bornIn
    }
    
    // 重寫decoding
    init(from decoder: Decoder) throws {
        // 通過指定映射規則來創建解碼容器,通過該容器獲取json中的數據,因此是個常量
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let name = try container.decode(String.self, forKey: .name)
        let age = try container.decode(Int.self, forKey: .age)
        let bornIn = try container.decode(String.self, forKey: .bornIn)
        self.init(name: name, age: age, bornIn: bornIn)
    }
    
    // 重寫encoding
    func encode(to encoder: Encoder) throws {
        // 通過指定映射規則來創建編碼碼容器,通過往容器里添加內容最后生成json,因此是個變量
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(bornIn, forKey: .bornIn)
    }

對于編碼和解碼的過程,我們都是創建一個容器,該容器有一個keyedBy的參數,用于指定屬性和json中key兩者間的映射的規則,因此這次我們傳CodingKeys的類型過去,說明我們要使用該規則來映射。對于解碼的過程,我們使用該容器來進行解碼,指定要值的類型和獲取哪一個key的值,同樣的,編碼的過程中,我們使用該容器來指定要編碼的值和該值對應json中的key,他們看起來有點像Dictionary的用法。還是使用上一篇的泛型函數來進行encode和decode:

func encode<T>(of model: T) throws where T: Codable {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let encodedData = try encoder.encode(model)
    print(String(data: encodedData, encoding: .utf8)!)
}
func decode<T>(of jsonString: String, type: T.Type) throws -> T where T: Codable {
    let data = jsonString.data(using: .utf8)!
    let decoder = JSONDecoder()
    let model = try decoder.decode(T.self, from: data)
    return model
}

現在我們來驗證我們重寫寫的是否正確:

let res = """
{
    "name": "Jone",
    "age": 17,
    "born_in": "China"
}
"""
let stu = try! decode(of: res, type: Student.self)
dump(stu)
try! encode(of: stu)
//? __lldb_expr_1.Student
//  - name: "Jone"
//  - age: 17
//  - bornIn: "China"
//{
//    "name" : "Jone",
//    "age" : 17,
//    "born_in" : "China"
//}

打印的結果是正確的,現在我們重寫的方法實現了和原生的一樣效果。


使用struct來遵守CodingKey來指定映射規則

接著我們倒回去看我們定義的模型,模型中定義的CodingKeys映射規則是用enum來遵守CodingKey協議實現的,其實我們還可以把CodingKeys的類型定義一個struct來實現CodingKey協議:

    // 映射規則,用來指定屬性和json中key兩者間的映射的規則
//    enum CodingKeys: String, CodingKey {
//        case name
//        case age
//        case bornIn = "born_in"
//    }
    
    // 映射規則,用來指定屬性和json中key兩者間的映射的規則
    struct CodingKeys: CodingKey {
        var stringValue: String //key
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
        
        // 在decode過程中,這里傳入的stringValue就是json中對應的key,然后獲取該key的值
        // 在encode過程中,這里傳入的stringValue就是生成的json中對應的key,然后設置key的值
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        // 相當于enum中的case
        static let name = CodingKeys(stringValue: "name")!
        static let age = CodingKeys(stringValue: "age")!
        static let bornIn = CodingKeys(stringValue: "born_in")!
    }

使用結構體來遵守該協議需要實現該協議的內容,這里因為我們的json中的key是String類型,所以用不到intValue,因此返回nil即可。重新運行,結果仍然是正確的。不過需要注意的是,如果 不是 使用enum來遵守CodingKey協議的話,例如用struct,我們 必須 重寫Codable協議里的編碼和解碼方法,否者就會報錯:

cannot automatically synthesize 'Decodable' because 'CodingKeys' is not an enum
cannot automatically synthesize 'Encodable' because 'CodingKeys' is not an enum

因此,使用struct來遵守CodingKey,比用enum工程量大。那為什么還要提出這種用法?因為在某些特定的情況下它還是有出場的機會,使用struct來指定映射規則更靈活,到在第三篇中的一個例子就會講到使用的場景,這里先明白它的工作方式。


自定義Encoding

在自定義encode中,我們需要注意的點是對時間格式處理,Optional值處理以及數組處理。

時間格式處理

上一篇文章也提及過關于對時間格式的處理,這里我們有兩個方法對時間格式進行自定義encode。

方法一:在encode方法中處理
struct Student: Codable {
    let registerTime: Date
    
    enum CodingKeys: String, CodingKey {
        case registerTime = "register_time"
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
        let stringDate = formatter.string(from: registerTime)
        try container.encode(stringDate, forKey: .registerTime)
    }
}
方法二: 對泛型函數中對JSONEncoder對象的dateEncodingStrategy屬性進行設置
encoder.dateEncodingStrategy = .custom { (date, encoder) in
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
        let stringDate = formatter.string(from: date)
        var container = encoder.singleValueContainer()
        try container.encode(stringDate)
    }

這里創建的容器是一個singleValueContainer,因為這里不像encode方法中那樣需要往容器里一直添加值,所以使用一個單值容器就可以了。

try! encode(of: Student(registerTime: Date()))
//{
//  "register_time" : "Nov-13-2017 20:12:57+0800"
//}

Optional值處理

如果模型中有屬性是可選值,并且為nil,當我進行encode時該值是不會以null的形式寫入json中:

struct Student: Codable {
    var scores: [Int]?
}
try! encode(of: Student())
//{
//
//}

因為系統對encode的實現其實不是像我們上面所以寫的那樣用container調用encode方法,而是調用encodeIfPresent這個方法,該方法對nil則不進行encode。我們可以強制將friends寫入json中:

struct Student: Codable {
    var scores: [Int]?
    
    enum CodingKeys: String, CodingKey {
        case scores
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(scores, forKey: .scores)
    }
}
try! encode(of: Student())
//{
//    "scores" : null
//}

數組處理

有時候,我們想對一個數組類型的屬性進行處理后再進行encode,或許你會想,使用一個compute property處理就可以了,但是你只是想將處理后的數組進行encode,原來的數組則不需要,于是你自定義encode來實現,然后!你突然就不想多寫一個compute property,只想在encode方法里進行處理,于是我們可以使用container的nestedUnkeyedContainer(forKey:)方法創建一個UnkeyedEncdingContainer(顧名思義,數組是沒有key的)來對于數組進行處理就可以了。

struct Student: Codable {
    let scores: [Int] = [66, 77, 88]
    
    enum CodingKeys: String, CodingKey {
        case scores
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // 創建一個對數組處理用的容器 (UnkeyedEncdingContainer)
        var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .scores)
        try scores.forEach {
            try unkeyedContainer.encode("\($0)分")
        }
    }
}
try! encode(of: Student())
//{
//    "scores" : [
//    "66分",
//    "77分",
//    "88分"
//    ]
//}

自定義Decoding

對于自定義decode操作上與自定義encode類似,需要說明的點同樣也是時間格式處理,數組處理,但Optional值就不用理會了。

時間格式處理

當我們嘗試寫出一下自定義decode代碼時就會拋出一個錯誤:

struct Student: Codable {
    let registerTime: Date
    
    enum CodingKeys: String, CodingKey {
        case registerTime = "register_time"
}

    init(registerTime: Date) {
        self.registerTime = registerTime
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let registerTime = try container.decode(Date.self, forKey: .registerTime)
        self.init(registerTime: registerTime)
    }
}

let res = """
{
    "register_time": "2017-11-13 22:30:15 +0800"
}
"""
let stu = try! decode(of: res, type: Student.self) ?
// error: Expected to decode Double but found a string/data instead.

因為我們這里時間的格式不是一個浮點數,而是有一定格式化的字符串,因此我們要進行對應的格式匹配,操作也是和自定義encode中的類似,修改init(from decoder: Decoder方法:

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let dateString = try container.decode(Date.self, forKey: .registerTime)
        let formaater = DateFormatter()
        formaater.dateFormat = "yyyy-MM-dd HH:mm:ss z"
        let registerTime = formaater.date(from: dateString)!
        self.init(registerTime: registerTime)
    }

或者我們可以在JSONDecoder對象對dateDncodingStrategy屬性使用custom來修改:

decoder.dateDecodingStrategy = .custom{ (decoder) -> Date in
        let container = try decoder.singleValueContainer()
        let dateString = try container.decode(String.self)
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
        return formatter.date(from: dateString)!
    }

數組處理

當我們獲取這樣的數據:

let res = """
{
    "gross_score": 120,
    "scores": [
        0.65,
        0.75,
        0.85
    ]
}
"""

gross_score代表該科目的總分數,scores里裝的是分數占總分數的比例,我們需要將它們轉換成實際的分數再進行初始化。對于數組的處理,我們和自定義encoding時所用的容器都是UnkeyedContainer,通過container的nestedUnkeyedContainer(forKey: )方法創建一個UnkeyedDecodingContainer,然后從這個unkeyedContainer中不斷取出值來decode,并指定其類型。

struct Student: Codable {
    let grossScore: Int
    let scores: [Float]
    
    enum CodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
    }
    
    init(grossScore: Int, scores: [Float]) {
        self.grossScore = grossScore
        self.scores = scores
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let grossScore = try container.decode(Int.self, forKey: .grossScore)
        
        var scores = [Float]()
        // 處理數組時所使用的容器(UnkeyedDecodingContainer)
        var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .scores)
        // isAtEnd:A Boolean value indicating whether there are no more elements left to be decoded in the container.
        while !unkeyedContainer.isAtEnd {
            let proportion = try unkeyedContainer.decode(Float.self)
            let score = proportion * Float(grossScore)
            scores.append(score)
        }
        self.init(grossScore: grossScore, scores: scores)
    }
}

扁平化JSON的編碼和解碼

現在我們已經熟悉了自定義encoding和decoding的過程了,也知道對數組處理要是container創建的nestedUnkeyedContainer(forKey: )創建的unkeyedContainer來處理。現在我們來看一個場景,假設我們有這樣一組含嵌套結構的數據:

let res = """
{
    "name": "Jone",
    "age": 17,
    "born_in": "China",
    "meta": {
        "gross_score": 120,
        "scores": [
            0.65,
            0.75,
            0.85
        ]
    }
}
"""

而我們定義的模型的結構卻是扁平的:

struct Student {
    let name: String
    let age: Int
    let bornIn: String
    let grossScore: Int
    let scores: [Float]
}

對于這類場景,我們可以使用container的nestedContainer(keyedBy:, forKey: )方法創建的KeyedContainer處理,同樣是處理內嵌類型的容器,既然有處理像數組這樣unkey的內嵌類型的容器,自然也有處理像字典這樣有key的內嵌類型的容器,在encoding中是KeyedEncodingContainer類型,而在decoding中當然是KeyedDecodingContainer類型,因為encoding和decoding中它們是相似的:

struct Student: Codable {
    let name: String
    let age: Int
    let bornIn: String
    let grossScore: Int
    let scores: [Float]
    
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case bornIn = "born_in"
        case meta
    }
    
    // 這里要指定嵌套的數據中的映射規則
    enum MetaCodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
    }


    init(name: String, age: Int, bornIn: String, grossScore: Int, scores: [Float]) {
        self.name = name
        self.age = age
        self.bornIn = bornIn
        self.grossScore = grossScore
        self.scores = scores
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let name = try container.decode(String.self, forKey: .name)
        let age = try container.decode(Int.self, forKey: .age)
        let bornIn = try container.decode(String.self, forKey: .bornIn)
        
        // 創建一個對字典處理用的容器 (KeyedDecodingContainer),并指定json中key和屬性名的規則
        let keyedContainer = try container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
        let grossScore = try keyedContainer.decode(Int.self, forKey: .grossScore)
        var unkeyedContainer = try keyedContainer.nestedUnkeyedContainer(forKey: .scores)
        var scores = [Float]()
        while !unkeyedContainer.isAtEnd {
            let proportion = try unkeyedContainer.decode(Float.self)
            let score = proportion * Float(grossScore)
            scores.append(score)
        }
        self.init(name: name, age: age, bornIn: bornIn, grossScore: grossScore, scores: scores)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(bornIn, forKey: .bornIn)
        
        // 創建一個對字典處理用的容器 (KeyedEncodingContainer),并指定json中key和屬性名的規則
        var keyedContainer = container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
        try keyedContainer.encode(grossScore, forKey: .grossScore)
        var unkeyedContainer = keyedContainer.nestedUnkeyedContainer(forKey: .scores)
        try scores.forEach {
            try unkeyedContainer.encode("\($0)分")
        }
    }
}

然后我們驗證一下:

let stu = try! decode(of: res, type: Student.self)
dump(stu)
try! encode(of: stu)
//? __lldb_expr_82.Student
//    - name: "Jone"
//    - age: 17
//    - bornIn: "China"
//    - grossScore: 120
//    ? scores: 3 elements
//        - 78.0
//        - 90.0
//        - 102.0
//
//{
//    "age" : 17,
//    "meta" : {
//        "gross_score" : 120,
//        "scores" : [
//        "78.0分",
//        "90.0分",
//        "102.0分"
//        ]
//    },
//    "born_in" : "China",
//    "name" : "Jone"
//}

現在我們實現了嵌套結構的json和扁平模型之間的轉換了。


至此我們學會了如何自定義encoding和decoding,其中的關鍵在與掌握container的使用,根據不同情況使用不同的container,實際情況千差萬別,可是套路總是類似,我們見招拆招就好了。

本文Demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容