Codable 學(xué)習(xí)

0x00 Codable 介紹

Codable是從Swift 4開始加入到Swift的標(biāo)準(zhǔn)庫組件呐馆,其提供了一種非常簡單的方式支持模型和數(shù)據(jù)之間的轉(zhuǎn)換稻励。

當(dāng)時(shí)官方發(fā)布的時(shí)候就說過靶橱,Codable的設(shè)計(jì)目標(biāo)有三個(gè):通用性、類型安全性以及減少編碼過程的模板代碼。

0x01 為什么要去學(xué)習(xí) Codable

  1. 解決序列化與反序列化
  2. 替代現(xiàn)有基于 ABI 不穩(wěn)定的方案
    • SwiftJSON
    • HandyJSON
    • KakaJSON

Swift發(fā)布4.0版本之前拱镐,官方未提供推薦的 JSON 處理方案,因此我們項(xiàng)目使用了HandyJSON 這套方案

但是, HandyJSON 的實(shí)現(xiàn)強(qiáng)依賴于Swift底層內(nèi)存布局機(jī)制持际,這個(gè)機(jī)制是非公開沃琅、不被承諾、且實(shí)踐證明一直在隨著Swift 版本變動(dòng)的蜘欲,HandyJSON 需要跟進(jìn) Swift 的每次版本更新益眉,更大的風(fēng)險(xiǎn)是,用戶升級(jí) iOS 版本可能會(huì)影響這個(gè)依賴姥份,導(dǎo)致應(yīng)用邏輯異常

0x02 怎么樣使用 Codable

代碼如下:

// 定義一個(gè)模型, 支持 Codable 協(xié)議
struct Person: Codable {
    let name: String
    let age: Int
    var test: Int?
}

// 解碼 JSON 數(shù)據(jù)
let json = #" {"name":"Tom", "age": 2} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)

print(person)

print("\n")

// 編碼導(dǎo)出為 JSON 數(shù)據(jù)
let data0 = try? JSONEncoder().encode(person)
let dataObject = try? JSONSerialization.jsonObject(with: data0!)
print(dataObject ?? "nil")

print("\n")

let data1 = try? JSONSerialization.data(withJSONObject: ["name": person.name, "age": person.age])
print(String(data: data1!, encoding: .utf8)!)

輸出結(jié)果如下:

Person(name: "Tom", age: 2, test: nil)


{
    age = 2;
    name = Tom;
}


{"name":"Tom","age":2}

0x03 分析與討論

上面的使用方式是一個(gè)最簡單的最直接的 Codable 引用, 其實(shí) Codable 還有很多使用方式以及問題

自定義 Key

使用以前的序列化工具都需要考慮的是自定義 Key, 比如服務(wù)器給的是 { "first_name": "Tom" }, 但是 APP 習(xí)慣是駝峰命名, 這時(shí)候就需要自定義 Key 了

當(dāng)然只是駝峰命名的話, 系統(tǒng)有封裝 decorder.keyDecodingStrategy = .convertFromSnakeCase 即可實(shí)現(xiàn), 后面的嵌套例子會(huì)用到, 其他的自定義 Key 就要自己實(shí)現(xiàn)了

struct Person: Codable {
    let name: String
    let age: Int
    let firstName: String
    
    enum CodingKeys: String, CodingKey {
        case name, age
        case firstName = "first_name"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        age = try values.decode(Int.self, forKey: .age)
        firstName = try values.decode(String.self, forKey: .firstName)
    }
    
    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(firstName, forKey: .firstName)
    }
}

let data = #"{"name": "Tom", "age": 10, "first_name": "James"}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
} catch {
    print(error)
}

Encoder 的三個(gè)接口

上面的 func encode(to encoder: Encoder) 里面使用了其中一個(gè), 以下是三個(gè)接口

如果模型想要 key -> value, 就使用
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey 接口作為容器, 這樣 encode 的結(jié)果就是一個(gè)可以轉(zhuǎn)換成字符串的 json data

如果模型想要忽略 key 值, 以 value 組成數(shù)組的方式 encode , 就使用 func unkeyedContainer() -> UnkeyedEncodingContainer 接口作為容器, 這樣的 encode 就是一個(gè)當(dāng)前層級(jí)為數(shù)組的 data

如果模型只想要在 encode 的時(shí)候保留其中一個(gè)值或者只有一個(gè)值的時(shí)候, 使用func singleValueContainer() -> SingleValueEncodingContainer 接口做為容器, 這樣 encode 的就是一個(gè)單一結(jié)果

// 還是上面的代碼

// 控制臺(tái)輸出 encode 結(jié)果, {"name": "Tom", "age": 10, "first_name": "James"}
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(firstName, forKey: .firstName)
}

// 控制臺(tái)輸出 encode 結(jié)果, ["Tom",10,"James"]
func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(name)
    try container.encode(age)
    try container.encode(firstName)
}

// 控制臺(tái)輸出 encode 結(jié)果, "Tom"
func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(name)
}

Embed 嵌套類型

最常見的數(shù)據(jù)結(jié)構(gòu), 一個(gè)模型內(nèi)有多個(gè)模型或者模型數(shù)組存在, 只要實(shí)現(xiàn)了 Codable 協(xié)議, 系統(tǒng)會(huì)自動(dòng)為我們完成嵌套內(nèi)容, 每一層只需要關(guān)心自己的 Codable 實(shí)現(xiàn)即可

struct Person: Codable, CustomStringConvertible {
    let name: String
    let age: Int
    
    var description: String {
        "name: \(name) age: \(age)"
    }
}

struct Family: Codable, CustomStringConvertible {
    let familyName: String
    let persons: [Person]
    
    var description: String {
        "familyName: \(familyName)\npersons: \(persons)"
    }
}


let data = """
{
    "family_name": "101",
    "persons":[
          {
             "name": "小明",
             "age": 1
          },
          {
             "name": "小紅",
             "age": 1
          }
    ]
}
""".data(using: .utf8)!

do {
    let decorder = JSONDecoder()
    decorder.keyDecodingStrategy = .convertFromSnakeCase
    let family = try decorder.decode(Family.self, from: data)
    print(family)
} catch {
    print(error)
}

輸出結(jié)果為:

familyName: 101
persons: [name: 小明 age: 1, name: 小紅 age: 1]

支持日期格式

只要滿足 formatter 格式的都會(huì)自動(dòng)轉(zhuǎn)換

struct Person: Codable {
    let birthday: Date
}

//let data = """
//{
//    "birthday": "2022-10-20T14:15:00-0000"
//}
//""".data(using: .utf8)!

//let data = """
//{
//    "birthday": 1666182937
//}
//""".data(using: .utf8)!

let data = """
{
    "birthday": "2022-10-19 20:35:37.000000"
}
""".data(using: .utf8)!

do {
    // create a date formatter
    let dateFormatter = DateFormatter()
    // set time zone
    dateFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai") ?? .current
    // setup formate string for the date formatter
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS"
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(dateFormatter)

    let person = try decoder.decode(Person.self, from: data)
    print(person)
    
    print(dateFormatter.string(from: person.birthday))
} catch {
    print(error)
}

網(wǎng)絡(luò)請(qǐng)求中序列化問題

網(wǎng)絡(luò)請(qǐng)求都會(huì)有能用枚舉表示的, Swift 的枚舉和 OC 的不一樣, 初始化不了就是 nil, 所以下面的代碼會(huì)報(bào)錯(cuò), dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "stageId", intValue: nil)], debugDescription: "Cannot initialize StageIDType from invalid Int value 99999", underlyingError: nil))

struct Person: Codable, CustomStringConvertible {
    enum Gender: Int, Codable {
        case male
        case female
    }

    enum StageIDType: Int, Codable {
        case preSchool = 9999
        case primary = 10001
        case junior = 10002
        case senior = 10003
    }

    let name: String
    var gender: Gender = .male
    var stageId: StageIDType = .junior

    var description: String {
        "name: \(name)\ngender: \(gender)\nstageId: \(stageId)"
    }
}

// 不給予默認(rèn)值, 會(huì)報(bào)數(shù)據(jù)錯(cuò)誤
// 給予默認(rèn)值, 依舊會(huì)數(shù)據(jù)錯(cuò)誤
let data = #"{"name": "123", "gender": 1, "stageId": 99999}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
} catch {
    print(error)
}

Codable 中意外值

上面的錯(cuò)誤, 可以對(duì)枚舉做二次封裝來匹配, 但是這樣的封裝使用起來會(huì)很費(fèi)事, 需要 switch 等方式取值, 變通一下這種意外方式, 使用結(jié)構(gòu)體+靜態(tài)屬性

struct Person: Codable, CustomStringConvertible {
    enum Gender: Int, Codable {
        case male
        case female
    }
    
    struct StageIDType: RawRepresentable, Codable {
        typealias RawValue = Int
        
        let rawValue: RawValue
        
        static let preSchool: StageIDType = .init(rawValue: 9999)
        static let primary: StageIDType = .init(rawValue: 1001)
        static let junior: StageIDType = .init(rawValue: 1002)
        static let senior: StageIDType = .init(rawValue: 1003)
    }
    
    let name: String
    let gender: Gender
    let stageId: StageIDType
    
    var description: String {
        "name: \(name)\ngender: \(gender)\nstageId: \(stageId)"
    }
}

let data = #"{"name": "123", "gender": 1, "stageId": 1004}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
    let data = try JSONEncoder().encode(person)
    print(String(data: data, encoding: .utf8)!)
} catch {
    print(error)
}

輸出如下:

name: 123
gender: female
stageId: StageIDType(rawValue: 1004)
{"name":"123","stageId":1004,"gender":1}

Codable 中默認(rèn)值

在開發(fā)的時(shí)候, 默認(rèn)值也很重要, 這里可以考慮用屬性包裝器, 做一下封裝來用, 只是一個(gè)思想, 關(guān)于 Codable 封裝好的三方庫也是有一些的, 至于用得上用不上就看開發(fā)人員自己選擇吧, 我們項(xiàng)目當(dāng)中暫時(shí)還沒有這種需求

struct Person: Codable {
    enum Gender: Int, Codable {
        case male
        case female
    }
    
    struct StageIDType: RawRepresentable, Codable {
        typealias RawValue = Int
        
        let rawValue: RawValue
        
        static let preSchool: StageIDType = .init(rawValue: 9999)
        static let primary: StageIDType = .init(rawValue: 1001)
        static let junior: StageIDType = .init(rawValue: 1002)
        static let senior: StageIDType = .init(rawValue: 1003)
    }
    
    let name: String
    let gender: Gender
    let stageId: StageIDType
    
    @Default<Bool>(true)
    var canPlayBall: Bool
}

protocol DefaultValuable {
    associatedtype Value: Codable
    static var defaultValue: Value { get }
}

extension Bool: DefaultValuable {
    static let defaultValue = true
}

extension Int: DefaultValuable {
    static var defaultValue: Int {
        100
    }
}

@propertyWrapper
struct Default<T: DefaultValuable>: Codable {
    var wrappedValue: T.Value
    
    init(_ wrappedValue: T.Value) {
        self.wrappedValue = wrappedValue
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

extension KeyedDecodingContainer {
    func decode<T>(
        _ type: Default<T>.Type,
        forKey key: Key
    ) throws -> Default<T> where T: DefaultValuable {
        return try decodeIfPresent(type, forKey: key) ?? Default(T.defaultValue)
    }
}

let data = #"{"name": "123", "gender": 1, "stageId": 1001, "canPlayBall": "12"}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
    
    let person1 = Person(name: "dsad", gender: .male, stageId: .senior)
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let data = try encoder.encode(person1)
    let json = String(data: data, encoding: .utf8)!
    print(json)
} catch {
    print(error)
}

輸出結(jié)果如下:

Person(name: "123", gender: __lldb_expr_119.Person.Gender.female, stageId: __lldb_expr_119.Person.StageIDType(rawValue: 1001), _canPlayBall: __lldb_expr_119.Default<Swift.Bool>(wrappedValue: true))
{
  "gender" : 0,
  "stageId" : 1003,
  "name" : "dsad",
  "canPlayBall" : true
}

0x04 總結(jié)

有興趣的可以看看下面引用中的源碼鏈接, 里面的代碼很好, 其中 Codable.swift 實(shí)現(xiàn)了接口協(xié)議以及基礎(chǔ)數(shù)據(jù)類型的 encoder decoder 的默認(rèn)實(shí)現(xiàn), JSONEncoder.swift 實(shí)現(xiàn)了具體功能

通過反射和內(nèi)存操作的那些庫, 比如 HandyJSON 優(yōu)勢(shì)是可以設(shè)置默認(rèn)值, 模型定義的 key 在 json 中不存在不會(huì)報(bào)錯(cuò), 會(huì)忽略, 而 Codable 需要可選值才能標(biāo)識(shí)忽略, Codable 不自定義 decode 都加問號(hào)(基礎(chǔ)數(shù)據(jù)類型), 或者自定義 decode 并添加默認(rèn)值

利用系統(tǒng)提供的便利性, 盡量在 Codable 處使用嵌套并擁有基礎(chǔ)數(shù)據(jù)類型, 這樣編譯器會(huì)在編譯的時(shí)候生成模版代碼

使用了 Codable 就盡量不要使用字典, Codable 意味著具象類型要以對(duì)象為單位, toJSONObject 的方式只用來給服務(wù)器上傳即可

多態(tài)的時(shí)候盡量使用協(xié)議來實(shí)現(xiàn)映射


引用:

Codable 使用小記

用 Codable 協(xié)議實(shí)現(xiàn)快速 JSON 解析

Codable源碼剖析

源碼解讀——Codable

Property Wrapper 為 Codable 解碼設(shè)定默認(rèn)值

如何優(yōu)雅的使用Swift Codable協(xié)議

Codable保姆級(jí)攻略

Codable.swift 源碼

JSONEncoder.swift 源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末郭脂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子澈歉,更是在濱河造成了極大的恐慌展鸡,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件埃难,死亡現(xiàn)場(chǎng)離奇詭異莹弊,居然都是意外死亡涤久,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門箱硕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拴竹,“玉大人,你說我怎么就攤上這事剧罩∷ò荩” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵惠昔,是天一觀的道長幕与。 經(jīng)常有香客問我,道長镇防,這世上最難降的妖魔是什么啦鸣? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮来氧,結(jié)果婚禮上诫给,老公的妹妹穿的比我還像新娘。我一直安慰自己啦扬,他們只是感情好中狂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扑毡,像睡著了一般胃榕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瞄摊,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天勋又,我揣著相機(jī)與錄音,去河邊找鬼换帜。 笑死楔壤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惯驼。 我是一名探鬼主播蹲嚣,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼跳座!你這毒婦竟也來了端铛?” 一聲冷哼從身側(cè)響起泣矛,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤疲眷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后您朽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狂丝,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡换淆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了几颜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倍试。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蛋哭,靈堂內(nèi)的尸體忽然破棺而出县习,到底是詐尸還是另有隱情,我是刑警寧澤谆趾,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布躁愿,位于F島的核電站,受9級(jí)特大地震影響沪蓬,放射性物質(zhì)發(fā)生泄漏彤钟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一跷叉、第九天 我趴在偏房一處隱蔽的房頂上張望逸雹。 院中可真熱鬧,春花似錦云挟、人聲如沸梆砸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辫樱。三九已至,卻和暖如春俊庇,著一層夾襖步出監(jiān)牢的瞬間狮暑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工辉饱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搬男,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓彭沼,卻偏偏與公主長得像缔逛,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姓惑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容