在iOS中最常見的工作是將數(shù)據(jù)保存起來并通過網(wǎng)絡(luò)傳輸。但是在這之前,你需要將數(shù)據(jù)通過編碼
或序列化
轉(zhuǎn)換成合適的格式醋粟。
同樣的,在你使用這些數(shù)據(jù)之前重归,你也需要將其轉(zhuǎn)換成合適的格式米愿。這個相反的過程被稱為解碼
或反序列化
。
在這個教程中鼻吮,你將學(xué)習(xí)到所有使用Swift進行編解碼所需要的知識育苟。包括這些:
- 在
蛇形命名
和駝峰命名
格式之間轉(zhuǎn)換 - 自定義
Coding keys
- 使用
keyed
,unkeyed
和nested
容器 - 處理
嵌套類型
,日期類型
以及子類
這確實有點多,是時候開始動手了椎木!
開始動手
鏈接:提取碼:15B7
下載完成后宙搬,starter是該教程使用的版本笨腥。final是最終完成的版本。
我們打開本節(jié)代碼Nested types
勇垛。使Toy
和Employee
遵循Codable
協(xié)議:
struct Toy: Codable {
...
}
struct Employee: Codable {
...
}
Codable
本身并不是一個協(xié)議,它只是另外兩個協(xié)議的別名:Encodable
和Decodable
士鸥。你也行已經(jīng)猜到了闲孤,這兩個協(xié)議就是代表那些可以被編解碼的類型。
你無需再做其他事情烤礁,因為Toy
和Employee
的所有存儲屬性都是Codable
的讼积。Swift標準庫中大多數(shù)類型(例如String
、URL
)都是支持Codable
的脚仔。
添加一個JSONEncoder
和JSONDecoder
來處理toys
和employees
的編解碼:
let encoder = JSONEncoder()
let decoder = JSONDecoder()
操作JSON我們只需做這些勤众!下面進入第一個挑戰(zhàn)!
編解碼嵌套類型
Employee
包含了一個Toy
屬性(這是個嵌套類型)鲤脏。編碼后的JSON結(jié)構(gòu)和Employee
結(jié)構(gòu)體保持一致:
{
"name" : "John Appleseed",
"id" : 7,
"favoriteToy" : {
"name" : "Teddy Bear"
}
}
public struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy
}
JSON數(shù)據(jù)將name
嵌套在favoriteToy
之中们颜,并且所有的JSON字段名與Toy
和Employee
的存儲屬性名相同,所以基于結(jié)構(gòu)體的類型體系猎醇,JSON的結(jié)構(gòu)很容易理解窥突。
如果屬性名稱和JSON的字段名都相同,并且屬性都是Codable
的硫嘶,那么我們可以很容易的將JSON轉(zhuǎn)換為數(shù)據(jù)模型阻问,或者反過來。現(xiàn)在來試一試:
// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!
這里做了2件事:
- 將
employee
使用encode(_:)
編碼成JSON沦疾。是不是很簡單称近! - 從上一步的
data
中創(chuàng)建String,一遍可以查看其內(nèi)容哮塞。
這里的編碼過程會產(chǎn)生合法的數(shù)據(jù)刨秆,所以我們可以使用它重新創(chuàng)建employee
:
let sameEmployee = try decoder.decode(Employee.self, from: data)
好了,可以開始下一個挑戰(zhàn)了彻桃!
在蛇形命名
和駝峰命名
格式之間轉(zhuǎn)換
現(xiàn)在坛善,假設(shè)JSON的鍵名從駝峰格式(這樣looksLikeThis
)轉(zhuǎn)換成了蛇形格式(這樣looks_like_this_instead
)。但是邻眷,Toy
和Employee
的存儲屬性只能使用駝峰格式眠屎。幸運的是Foundation
考慮到了這種情況。
打開本節(jié)代碼Snake case vs camel case
肆饶,在編解碼器創(chuàng)建之后使用之前的位置添加下面的代碼:
encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
運行代碼改衩,檢查snakeString
,編碼后的employee
產(chǎn)生下面的內(nèi)容:
{
"name" : "John Appleseed",
"id" : 7,
"favorite_toy" : {
"name" : "Teddy Bear"
}
}
自定義Coding keys
現(xiàn)在驯镊,假設(shè)JOSN的格式再一次改變葫督,其使用的字段名和Toy
和Employee
中存儲屬性名不一致了:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"name" : "Teddy Bear"
}
}
可以看到竭鞍,這里使用gift
代替了原來的favoriteToy
。這種情況我們需要自定義Coding keys
橄镜。在我們的類型中添加一個特殊的枚舉類型偎快。打開本節(jié)代碼Custom coding keys
,在Employee
中添加下面的代碼:
enum CodingKeys: String, CodingKey {
case name, id, favoriteToy = "gift"
}
這個特殊的枚舉遵循了CodingKey
協(xié)議洽胶,并使用String
類型的原始值晒夹。在這里我們可以讓favoriteToy
和gift
一一對應(yīng)起來。
在編解碼過程中姊氓,只會操作出現(xiàn)在枚舉中的cases
丐怯,所以即使那些不需要指定一一對應(yīng)的屬性,也需要在枚舉中包含翔横,就像這里的name
和id
读跷。
運行playground
,然后查看string
的值禾唁,你會發(fā)現(xiàn)JSON字段名不在依賴存儲屬性名稱效览,這得益于自定義的Coding keys
。
繼續(xù)下一個挑戰(zhàn)蟀俊!
處理扁平化
的JSON
現(xiàn)在钦铺,JSON的格式變成下面這樣:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
這里不在有嵌套
結(jié)構(gòu),和我們的模型結(jié)構(gòu)不一致了肢预。這種情況我們需要自定義編解碼過程矛洞。
打開本節(jié)代碼Keyed containers
。這里有個Employee
類型烫映,它遵循了Encodable
沼本。同時我們使用extension
讓它遵循了Decodable
。
這樣做的好處是锭沟,可以保留結(jié)構(gòu)體的逐一成員構(gòu)造器
抽兆。如果我們在定義Employee
時讓它遵循Decodable
,它將失去這個構(gòu)造器族淮。添加下面的代碼到Employee
中:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
func encode(to encoder: Encoder) throws {
// 2
var container = encoder.container(keyedBy: CodingKeys.self)
// 3
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
try container.encode(favoriteToy.name, forKey: .gift)
}
在之前簡單(指屬性名和鍵名一一對應(yīng)且嵌套層級相同)的示例中辫红,encode(to:)
方法由編譯器自動實現(xiàn)了。現(xiàn)在我們需要手動實現(xiàn)祝辣。
- 創(chuàng)建
CodingKeys
表示JSON的字段贴妻。因為我們沒有做任何的關(guān)系映射,所以不必聲明它的原始類型為String
蝙斜。 - 從
encoder
中獲取KeyedEncodingContainer
容器名惩。這就像一個字典,我們可以存儲屬性的值到其中孕荠,這樣就進行了編碼娩鹉。 - 編碼
name
和id
屬性到容器中攻谁。 - 使用
gift
鍵,直接將toy
的名字編碼到容器中弯予。
運行playground
戚宦,然后查看string
的值,你會發(fā)現(xiàn)它符合上面JSON的格式熙涤。我們可以選擇使用什么字段名編碼一個屬性值阁苞,這給了我們很大的靈活性。
和編碼過程類似祠挫,簡單版本的init(from:)
方法可以由編譯器自動實現(xiàn)。但是這里我們需要手動實現(xiàn)悼沿,使用下面的代碼替換fatalError("To do")
:
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)
然后添加下面的代碼等舔,就可以從JSON中重新創(chuàng)建employee
:
let sameEmployee = try decoder.decode(Employee.self, from: data)
處理多級嵌套的JSON
現(xiàn)在,JSON的格式變成下面這樣:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
name
字段在toy
字段中糟趾,而toy
又在gift
字段中慌植。如何解析成我們定義的數(shù)據(jù)模型呢?
打開本節(jié)代碼Nested keyed containers
义郑,添加下面的代碼到Employee
:
// 1
enum CodingKeys: CodingKey {
case name, id, gift
}
// 2
enum GiftKeys: CodingKey {
case toy
}
// 3
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
// 4
var giftContainer = container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
try giftContainer.encode(favoriteToy, forKey: .toy)
}
這里做了幾件事:
- 創(chuàng)建頂層的
CodingKeys
- 創(chuàng)建用于解析
gift
字段的CodingKeys
蝶柿,后續(xù)使用它創(chuàng)建容器 - 使用頂層容器編碼
name
和id
- 使用
nestedContainer(keyedBy:forKey:)
方法獲取用于編碼gift
字段的容器,并將favoriteToy
編碼進去
運行并查看string
的值非驮,你會發(fā)現(xiàn)JSON的格式符合預(yù)期交汤。
解碼過程也很類似。添加下面的代碼:
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
let giftContainer = try container
.nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
}
}
let sameEmployee = try decoder.decode(Employee.self, from: nestedData)
好了劫笙,我們已經(jīng)搞定了嵌套類型的容器芙扎。并從其中解碼出了sameEmployee
。
處理日期類型
現(xiàn)在填大,JSON里添加了日期字段戒洼,就像下面這樣:
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : "29-05-2019",
"toy" : {
"name" : "Teddy Bear"
}
}
JSON中并沒有標準的日期格式。在JSONEncoder
和JSONDecoder
使用日期類的timeIntervalSinceReferenceDate
方法去處理(Date(timeIntervalSinceReferenceDate: interval)
)允华。
這里我們需要指定日期轉(zhuǎn)換策略
圈浇。打開本節(jié)代碼Dates
,在try encoder.encode(employee)
之前添加下面的代碼:
// 1
extension DateFormatter {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
return formatter
}()
}
// 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
這里主要做了2件事:
- 在
DateFormatter
的擴展中添加了格式化器
靴寂,它的格式化形式滿足JSON中日期的格式磷蜀,并且是可以重用的。 - 設(shè)置
dateEncodingStrategy
和dateDecodingStrategy
為.formatted(.dateFormatter)
榨汤,這樣編解碼時就會使用它去處理日期
運行并檢查dateString
的內(nèi)容蠕搜,你會發(fā)現(xiàn)它符合預(yù)期。
處理子類
現(xiàn)在收壕,JSON格式變成了下面這樣:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
這里將Employee
所需信息分開了妓灌。我們打算使用BasicEmployee
去解析employee
轨蛤。打開本節(jié)代碼Subclasses
,使BasicEmployee
遵循Codable
:
class BasicEmployee: Codable {
不出意外虫埂,編譯器報錯了祥山,因為GiftEmployee
并沒有遵循Codable
。我們繼續(xù)添加下面的代碼掉伏,就可以修正錯誤了:
// 1
enum CodingKeys: CodingKey {
case employee, birthday, toy
}
// 2
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
birthday = try container.decode(Date.self, forKey: .birthday)
toy = try container.decode(Toy.self, forKey: .toy)
// 3
let baseDecoder = try container.superDecoder(forKey: .employee)
try super.init(from: baseDecoder)
}
這里做了3件事:
- 在
GiftEmployee
中添加了CodingKeys
缝呕。和JSON
中的字段名對應(yīng)。 - 從
decoder
解碼出子類的屬性值斧散。 - 創(chuàng)建用于解碼父類屬性的
Decoder
供常,然后調(diào)用父類的方法初始化父類屬性。
下面我們繼續(xù)完成GiftEmployee
的編碼方法:
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
let baseEncoder = container.superEncoder(forKey: .employee)
try super.encode(to: baseEncoder)
}
和解碼過程類似鸡捐,我們先編碼了子類的屬性栈暇,然后獲取用于編碼父類的encoder
。下面測試下結(jié)果:
let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(), toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)
運行并檢查giftString
箍镜,你會發(fā)現(xiàn)其內(nèi)容符合預(yù)期源祈。學(xué)習(xí)了本節(jié),你就可以處理更復(fù)雜的繼承數(shù)據(jù)模型了色迂。
處理混合類型的數(shù)組
現(xiàn)在香缺,JSON格式變成了下面這樣:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
這是個JSON數(shù)組,但是其內(nèi)部元素格式并不一致歇僧。打開本節(jié)代碼Polymorphic types
图张,可以看到這里使用枚舉定義了不同類型的數(shù)據(jù)。
首先馏慨,我們讓AnyEmployee
遵循Encodable
協(xié)議:
enum AnyEmployee: Encodable { ... }
繼續(xù)在AnyEmployee
中添加下面的代碼:
// 1
enum CodingKeys: CodingKey {
case name, id, birthday, toy
}
// 2
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .defaultEmployee(let name, let id):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
case .customEmployee(let name, let id, let birthday, let toy):
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(birthday, forKey: .birthday)
try container.encode(toy, forKey: .toy)
case .noEmployee:
let context = EncodingError.Context(codingPath: encoder.codingPath,
debugDescription: "Invalid employee!")
throw EncodingError.invalidValue(self, context)
}
}
這里我們主要做了兩件事:
- 定義了所有可能的鍵埂淮。
- 根據(jù)不同類型,對數(shù)據(jù)進行編碼写隶。
在代碼的最后添加下面的內(nèi)容來進行測試:
let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7),
AnyEmployee.customEmployee("John Appleseed", 7, Date(),toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!
接下來的編碼過程有點復(fù)雜倔撞。繼續(xù)添加下面的代碼:
extension AnyEmployee: Decodable {
init(from decoder: Decoder) throws {
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerKeys = Set(container.allKeys)
let defaultKeys = Set<CodingKeys>([.name, .id])
let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
// 2
switch containerKeys {
case defaultKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
self = .defaultEmployee(name, id)
case customKeys:
let name = try container.decode(String.self, forKey: .name)
let id = try container.decode(Int.self, forKey: .id)
let birthday = try container.decode(Date.self, forKey: .birthday)
let toy = try container.decode(Toy.self, forKey: .toy)
self = .customEmployee(name, id, birthday, toy)
default:
self = .noEmployee
}
}
}
// 3
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
解釋下上面的代碼:
- 獲取
KeydContainer
,并獲取其所有鍵慕趴。 - 根據(jù)不同的鍵痪蝇,實行不同的解析策略
- 從
employeesData
中解碼出[AnyEmployee]
個人感覺若數(shù)組中的元素可以用同一模型來表示,只是字段可能為空時冕房,直接將模型字段設(shè)為可選躏啰。當然這里也提供了解析不同模型的思路。
處理數(shù)組
現(xiàn)在耙册,我們有如下格式JSON:
[
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
這里是一個數(shù)組给僵,并且其大小寫各不相同。此時我們不需要任何CodingKey
,只需使用unkeyed container
帝际。
打開本節(jié)代碼Unkeyed containers
蔓同,添加下面的代碼到Label
結(jié)構(gòu)體中:
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(toy.name.lowercased())
try container.encode(toy.name.uppercased())
try container.encode(toy.name)
}
UnkeyedEncodingContainer
和之前用到的KeyedEncodingContainer
相似,但是它不需要CodingKey
蹲诀,因為它將編碼數(shù)據(jù)寫入JSON數(shù)組中斑粱。這里我們編碼了3中不同的字符串到其中。
繼續(xù)解碼:
extension Label: Decodable {
// 1
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var name = ""
while !container.isAtEnd {
name = try container.decode(String.self)
}
toy = Toy(name: name)
}
}
let sameLabel = try decoder.decode(Label.self, from: labelData)
這里主要是獲取decoder.unkeyedContainer
脯爪,獲取容器中最后一個值來初始化name
则北。
處理嵌套在對象中的數(shù)組
現(xiàn)在我們有如下格式JSON:
{
"name" : "Teddy Bear",
"label" : [
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
}
這次,標簽對應(yīng)在了label
字段下痕慢。我們需要使用nested unkeyed containers
去進行編解碼尚揣。
打開本節(jié)代碼Nested unkeyed containers
,在Toy
中添加下面的代碼:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var labelContainer = container.nestedUnkeyedContainer(forKey: .label)
try labelContainer.encode(name.lowercased())
try labelContainer.encode(name.uppercased())
try labelContainer.encode(name)
}
這里我們創(chuàng)建了一個nested unkeyed container
掖举,并填充了3個字符串惑艇。運行代碼,并查看string
的值拇泛,可以看到預(yù)期結(jié)果。
繼續(xù)添加下面的代碼進行解碼:
extension Toy: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
var labelName = ""
while !labelContainer.isAtEnd {
labelName = try labelContainer.decode(String.self)
}
label = labelName
}
}
let sameToy = try decoder.decode(Toy.self, from: data)
這里思灌,我們像之前一樣俺叭,使用unkeyed container
的最后一個值初始化label
字段,只不過獲取的是嵌套的容器泰偿。
處理可選字段
最后熄守,我們的模型中的屬性也可以是可選類型,container
也提供了對應(yīng)的編解碼方法:
encodeIfPresent(value, forKey: key)
decodeIfPresent(type, forKey: key)
總結(jié)
今天我們由淺入深的學(xué)習(xí)了如何在Swift
中處理JSON
耗跛。其中自定義Coding keys
裕照、處理子類
等部分需要重點理解。希望對大家有所幫助调塌。
原文鏈接:
https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift