Swift4中Codable的使用(二)

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


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

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模型來進(jìn)行演示:

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

重寫系統(tǒng)的方法,實(shí)現(xiàn)與系統(tǒng)一樣的decode和encode效果

在自定義前,我們先來把這兩個方法重寫成系統(tǒng)默認(rèn)的實(shí)現(xiàn)來了解一下,對于這兩個方法,我們要掌握的是container的用法。

    init(name: String, age: Int, bornIn: String) {
        self.name = name
        self.age = age
        self.bornIn = bornIn
    }
    
    // 重寫decoding
    init(from decoder: Decoder) throws {
        // 通過指定映射規(guī)則來創(chuàng)建解碼容器,通過該容器獲取json中的數(shù)據(jù),因此是個常量
        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 {
        // 通過指定映射規(guī)則來創(chuàng)建編碼碼容器,通過往容器里添加內(nèi)容最后生成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)
    }

對于編碼和解碼的過程,我們都是創(chuàng)建一個容器,該容器有一個keyedBy的參數(shù),用于指定屬性和json中key兩者間的映射的規(guī)則,因此這次我們傳CodingKeys的類型過去,說明我們要使用該規(guī)則來映射。對于解碼的過程,我們使用該容器來進(jìn)行解碼,指定要值的類型和獲取哪一個key的值,同樣的,編碼的過程中,我們使用該容器來指定要編碼的值和該值對應(yīng)json中的key,他們看起來有點(diǎn)像Dictionary的用法。還是使用上一篇的泛型函數(shù)來進(jìn)行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
}

現(xiàn)在我們來驗(yàn)證我們重寫寫的是否正確:

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"
//}

打印的結(jié)果是正確的,現(xiàn)在我們重寫的方法實(shí)現(xiàn)了和原生的一樣效果。


使用struct來遵守CodingKey來指定映射規(guī)則

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

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

使用結(jié)構(gòu)體來遵守該協(xié)議需要實(shí)現(xiàn)該協(xié)議的內(nèi)容,這里因?yàn)槲覀兊膉son中的key是String類型,所以用不到intValue,因此返回nil即可。重新運(yùn)行,結(jié)果仍然是正確的。不過需要注意的是,如果 不是 使用enum來遵守CodingKey協(xié)議的話,例如用struct,我們 必須 重寫Codable協(xié)議里的編碼和解碼方法,否者就會報錯:

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

因此,使用struct來遵守CodingKey,比用enum工程量大。那為什么還要提出這種用法?因?yàn)樵谀承┨囟ǖ那闆r下它還是有出場的機(jī)會,使用struct來指定映射規(guī)則更靈活,到在第三篇中的一個例子就會講到使用的場景,這里先明白它的工作方式。


自定義Encoding

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

時間格式處理

上一篇文章也提及過關(guān)于對時間格式的處理,這里我們有兩個方法對時間格式進(jìn)行自定義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)
    }
}
方法二: 對泛型函數(shù)中對JSONEncoder對象的dateEncodingStrategy屬性進(jìn)行設(shè)置
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)
    }

這里創(chuàng)建的容器是一個singleValueContainer,因?yàn)檫@里不像encode方法中那樣需要往容器里一直添加值,所以使用一個單值容器就可以了。

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

Optional值處理

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

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

因?yàn)橄到y(tǒng)對encode的實(shí)現(xiàn)其實(shí)不是像我們上面所以寫的那樣用container調(diào)用encode方法,而是調(diào)用encodeIfPresent這個方法,該方法對nil則不進(jìn)行encode。我們可以強(qiáng)制將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
//}

數(shù)組處理

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

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)
        // 創(chuàng)建一個對數(shù)組處理用的容器 (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類似,需要說明的點(diǎn)同樣也是時間格式處理,數(shù)組處理,但Optional值就不用理會了。

時間格式處理

當(dāng)我們嘗試寫出一下自定義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.

因?yàn)槲覀冞@里時間的格式不是一個浮點(diǎn)數(shù),而是有一定格式化的字符串,因此我們要進(jìn)行對應(yīng)的格式匹配,操作也是和自定義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)!
    }

數(shù)組處理

當(dāng)我們獲取這樣的數(shù)據(jù):

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

gross_score代表該科目的總分?jǐn)?shù),scores里裝的是分?jǐn)?shù)占總分?jǐn)?shù)的比例,我們需要將它們轉(zhuǎn)換成實(shí)際的分?jǐn)?shù)再進(jìn)行初始化。對于數(shù)組的處理,我們和自定義encoding時所用的容器都是UnkeyedContainer,通過container的nestedUnkeyedContainer(forKey: )方法創(chuàng)建一個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]()
        // 處理數(shù)組時所使用的容器(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的編碼和解碼

現(xiàn)在我們已經(jīng)熟悉了自定義encoding和decoding的過程了,也知道對數(shù)組處理要是container創(chuàng)建的nestedUnkeyedContainer(forKey: )創(chuàng)建的unkeyedContainer來處理?,F(xiàn)在我們來看一個場景,假設(shè)我們有這樣一組含嵌套結(jié)構(gòu)的數(shù)據(jù):

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

而我們定義的模型的結(jié)構(gòu)卻是扁平的:

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

對于這類場景,我們可以使用container的nestedContainer(keyedBy:, forKey: )方法創(chuàng)建的KeyedContainer處理,同樣是處理內(nèi)嵌類型的容器,既然有處理像數(shù)組這樣unkey的內(nèi)嵌類型的容器,自然也有處理像字典這樣有key的內(nèi)嵌類型的容器,在encoding中是KeyedEncodingContainer類型,而在decoding中當(dāng)然是KeyedDecodingContainer類型,因?yàn)閑ncoding和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
    }
    
    // 這里要指定嵌套的數(shù)據(jù)中的映射規(guī)則
    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)
        
        // 創(chuàng)建一個對字典處理用的容器 (KeyedDecodingContainer),并指定json中key和屬性名的規(guī)則
        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)
        
        // 創(chuàng)建一個對字典處理用的容器 (KeyedEncodingContainer),并指定json中key和屬性名的規(guī)則
        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)分")
        }
    }
}

然后我們驗(yàn)證一下:

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"
//}

現(xiàn)在我們實(shí)現(xiàn)了嵌套結(jié)構(gòu)的json和扁平模型之間的轉(zhuǎn)換了。


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

本文Demo

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

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,725評論 25 709
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,253評論 6 342
  • Swift版本點(diǎn)擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,990評論 7 249
  • 那時 雨下的很大 你撐著傘在雨里 一個人走了很遠(yuǎn) 那時 你說你最喜歡下雨天 在雨中曼舞 在水中漫步 那時 你天真的...
    漀杌閱讀 299評論 0 0

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