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
- 解決序列化與反序列化
- 替代現(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 協(xié)議實(shí)現(xiàn)快速 JSON 解析
Property Wrapper 為 Codable 解碼設(shè)定默認(rèn)值