版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2019.07.22 星期一 |
前言
這個專題我們就一起看一下Swfit相關的基礎知識。感興趣的可以看上面幾篇。
1. Swift基礎知識相關(一) —— 泛型(一)
開始
首先看下主要內容
主要內容:在本教程中,您將學習Swift中的所有編碼和解碼,探索自定義日期和自定義編碼等基礎知識和高級主題。
然后看些寫作環境
Swift 5, iOS 12, Xcode 10
iOS應用程序的一項常見任務是保存數據并通過網絡發送數據。 但在此之前,您需要通過稱為編碼或序列化(encoding or serialization)
的過程將數據轉換為合適的格式。
在應用中使用之前,您還需要將通過網絡發送的已保存數據轉換為合適的格式。 該反向過程稱為解碼或反序列化(decoding or deserialization)
。
在本教程中,您將通過管理自己的toy store
了解有關Swift編碼和解碼的所有信息。 您將在此過程中探索以下主題:
- 在
snake case and camel case
之間切換。 - 定義自定義編碼key。
- 使用
keyed, unkeyed and nested containers
。 - 處理嵌套類型,日期,子類和多態類型。
有很多東西可以了解,所以是時候開始了!
注意:本教程假定您具有
JSON
的基本知識。 如果您需要快速瀏覽,請查看此 cheat sheet。
打開起始項目,通過轉到View ? Navigators ? Show Project Navigator
,確保在Xcode中可以看到Project navigator
。 打開Nested types
。
為Toy
和Employee
添加Codable
遵守:
struct Toy: Codable {
...
}
struct Employee: Codable {
...
}
Codable
本身不是協議,而是另外兩個協議的別名:Encodable
和Decodable
。 正如您可能猜到的那樣,這兩個協議聲明類型可以編碼為不同的格式并從其中解碼。
您不需要再做任何事情,因為Toy
和Employee
的所有存儲屬性(stored properties)
都是可編碼(codable)
的。 默認情況下,Swift標準庫和基礎類型(Swift Standard Library and Foundation )
中的許多基本類型(例如,String
和URL
)都是可編碼的。
注意:您可以將可編碼類型編碼為各種格式,例如
Property Lists (PLists)
,XML
或JSON
,但是對于本教程,您只能使用JSON。
添加JSONEncoder
和JSONDecoder
來處理toys
和employees
的JSON
編碼和解碼:
let encoder = JSONEncoder()
let decoder = JSONDecoder()
這就是使用JSON所需的全部內容。 第一次編碼和解碼挑戰的時間!
Encoding and Decoding Nested Types
Employee
包含一個Toy
屬性 - 它是一個嵌套類型(nested type)
。 編碼employee
的JSON結構與Employee
結構匹配:
{
"name" : "John Appleseed",
"id" : 7,
"favoriteToy" : {
"name" : "Teddy Bear"
}
}
public struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy
}
JSON
在favoriteToy
中嵌套name
,所有JSON鍵與Employee
和Toy
存儲屬性相同,因此您可以根據數據類型層次結構輕松理解JSON結構。 如果您的屬性名稱與您的JSON字段名稱匹配,并且您的屬性都是Codable
,那么您可以非常輕松地轉換為JSON或從JSON轉換。 你現在就試試。
Gifts
部門為員工提供他們喜歡的玩具作為生日禮物。 添加以下代碼以將員工的數據發送到禮品部門:
// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!
以下是此代碼的工作原理:
- 1) 使用
encode(_:)
將employee
編碼為JSON
(我告訴過你這很簡單!)。 - 2) 從編碼
data
創建一個字符串以使其可視化。
注意:按
Shift-Return
可將playground
運行到當前行,或單擊藍色play
按鈕。 要查看結果,可以將值打印Show Result
按鈕。
編碼過程生成有效數據,因此禮品部門可以重新創建員工:
let sameEmployee = try decoder.decode(Employee.self, from: data)
在這里,您已經使用decode(_:from :)
將data
解碼回Employee
......您已經讓您的員工非常開心。 按藍色play
按鈕以運行Playground
并查看結果。
是時候進行下一次挑戰!
Switching Between Snake Case and Camel Case Formats
禮品部門API已經從camel case
(looksLikeThis
)轉換到snake case
(looks_like_this_instead
)以格式化其JSON的鍵。
但是Employee
和Toy
的所有存儲屬性都只使用camel case
的情況! 幸運的是,Foundation
為您提供服務。
打開Snake case vs camel case
并在創建編碼器和解碼器之后添加以下代碼,然后再使用它們:
encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
在這里,您將keyEncodingStrategy
設置為.convertToSnakeCase
以對employee
進行編碼。 您還將keyDecodingStrategy
設置為.convertFromSnakeCase
以解碼snakeData
。
運行playground
并檢查snakeString
。 在這種情況下,編碼的employee
看起來像這樣(雙關語):
{
"name" : "John Appleseed",
"id" : 7,
"favorite_toy" : {
"name" : "Teddy Bear"
}
}
JSON中的格式現在是favorite_toy
,并且您已將其轉換回Employee
結構中的favoriteToy
。 你再次保存了(員工的出生日!)
Working With Custom JSON Keys
禮品部門再次更改其API
以使用與您的Employee
和Toy
存儲屬性不同的JSON key
:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"name" : "Teddy Bear"
}
}
現在,API
用gift
取代了favoriteToy
。
這意味著JSON
中的字段名稱將不再與您的類型中的屬性名稱匹配。 您可以定義自定義編碼鍵(custom coding keys )
以提供屬性的編碼名稱。 您可以通過向類型添加特殊枚舉來完成此操作。 打開custom coding keys
并在Employee
類型中添加此代碼:
enum CodingKeys: String, CodingKey {
case name, id, favoriteToy = "gift"
}
CodingKeys
是上面提到的特殊枚舉。 它符合CodingKey
并具有String
原始值。 這里是您將favoriteToy
映射到gift
的地方。
如果此枚舉存在,則只有此處出現的情況將用于編碼和解碼,因此即使您的屬性不需要映射,它也必須包含在枚舉中,如name
和id
在此處所示。
運行playground
并查看編碼的字符串值 - 您將看到正在使用的新字段名稱。 由于自定義編碼密鑰custom coding keys
,JSON不再依賴于您存儲的屬性。
是時候進行下一次挑戰!
Working With Flat JSON Hierarchies
現在,Gifts部門的API不希望其JSON中有任何嵌套類型,因此它們的代碼如下所示:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : "Teddy Bear"
}
這與您的模型結構不匹配,因此您需要編寫自己的編碼邏輯并描述如何編碼每個Employee
和Toy
存儲的屬性。
首先,打開Keyed containers
。 您將看到一個聲明為Encodable
的Employee
類型。 它也在擴展中聲明為Decodable
。 這種拆分是為了保持你使用Swift結構體獲得的free member-wise
初始化程序。 如果在主定義中聲明了init
方法,則會丟失該方法。 在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(toy.name, forKey: .gift)
}
對于您在上面看到的簡單情況,編譯器會自動為您實現encode(to :)
。 現在,你自己做了。 這是代碼正在做的事情:
- 1) 創建一組編碼鍵來表示您的
JSON
字段。 因為您沒有進行任何映射,所以您不需要將它們聲明為字符串,因為沒有原始值。 - 2) 創建
KeyedEncodingContainer
。 這就像您可以在編碼時存儲屬性的字典。 - 3) 將
name
和id
屬性直接編碼到容器中。 - 4) 使用禮品密鑰將
toy
的名稱直接編碼到容器中
運行playground
并檢查編碼字符串的值 - 它將匹配本節頂部的JSON
。 能夠選擇對哪些鍵進行編碼的屬性為您提供了很大的靈活性。
解碼過程與編碼過程相反。 用這個替換可怕的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)
與編碼一樣,對于簡單的情況,編譯器會自動為您生成init(from :)
,但是您自己就是這樣做的。 這是代碼正在做的事情:
- 1) 從解碼器獲取一個鍵控容器,它將包含JSON中的所有屬性。
- 2) 使用適當的類型和編碼key從容器中提取
name and id
。 - 3) 提取禮物的名稱,并使用它來構建
Toy
并將其分配給正確的屬性。
添加一行以從平面JSON重新創建employee
:
let sameEmployee = try decoder.decode(Employee.self, from: data)
這一次,您選擇了哪些屬性來解碼哪些鍵,并有機會在解碼過程中進一步工作。 手動編碼和解碼功能強大,為您提供靈活性。 您將在接下來的挑戰中了解更多相關信息。
Working With Deep JSON Hierarchies
禮品部門希望確保員工的生日禮物只能是玩具,因此其API會生成如下所示的JSON:
{
"name" : "John Appleseed",
"id" : 7,
"gift" : {
"toy" : {
"name" : "Teddy Bear"
}
}
}
你在toy
和gift
里面的toy
同時嵌入name
。 與Employee
層次結構相比,JSON結構添加了額外級別的縮進,因此在這種情況下您需要使用嵌套的鍵控容器(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(toy, forKey: .toy)
}
這就是上面代碼的工作原理:
- 1) 創建
top-level coding keys
。 - 2) 創建另一組編碼鍵,您將使用它來創建另一個容器。
- 3) 按照您習慣的方式對
name and id
進行編碼。 - 4) 創建一個嵌套容器
nestedContainer(keyedBy:forKey :)
并用它編碼toy
。
運行playground
并檢查編碼的字符串以查看多級JSON。 您可以使用盡可能多的嵌套容器,因為您的JSON具有縮進級別。 在現實世界的API中使用復雜而深入的JSON層次結構時,這很方便。
在這種情況下,解碼很簡單。 添加以下擴展名:
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)
您已使用嵌套解碼容器(nested decoding container)
將nestedData
解碼為Employee
。
Encoding and Decoding Dates
禮品部門需要知道員工的生日才能發送禮物,因此他們的JSON看起來像這樣:
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : "29-05-2019",
"toy" : {
"name" : "Teddy Bear"
}
}
日期沒有JSON
標準,這對于每個與之合作過的程序員而言都是如此。 JSONEncoder
和JSONDecoder
默認使用日期的timeIntervalSinceReferenceDate
的雙重表示,這在并不常見。
您需要使用日期策略(date strategy)
。 在try encoder.encode(employee)
語句之前,將此代碼塊添加到日期(Dates)
:
// 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)
這是代碼的作用:
- 1) 創建與所需格式匹配的日期格式化程序。 它作為
DateFormatter
的靜態屬性添加,因為這是您的代碼的良好實踐,因此格式化程序是可重用的。 - 2) 將
dateEncodingStrategy
和dateDecodingStrategy
設置為.formatted(.dateFormatter)
,告訴編碼器和解碼器在編碼和解碼日期時使用格式化程序。
檢查dateString
并檢查日期格式是否正確。 您已確保禮品部門將按時交付禮品 - 即將推出!
還有一些挑戰,你已經完成了。
Encoding and Decoding Subclasses
Gifts部門API可以根據類層次結構(class hierarchies)
處理JSON:
{
"toy" : {
"name" : "Teddy Bear"
},
"employee" : {
"name" : "John Appleseed",
"id" : 7
},
"birthday" : 580794178.33482599
}
employee
匹配沒有toy or birthday
的基類結構。 打開Subclasses
并使BasicEmployee
符合Codable
:
class BasicEmployee: Codable {
這會給你一個錯誤,因為GiftEmployee
還不是Codable
。 通過向GiftEmployee
添加以下內容來糾正:
// 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)
}
此代碼涵蓋解碼:
- 1) 添加相關的編碼密鑰。
- 2) 解碼特定于子類的屬性。
- 3) 使用
superDecoder(forKey :)
獲取一個適合傳遞給超類的init(from :)
方法的解碼器實例,然后初始化超類。
現在在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)
}
它是相同的模式,但您使用superEncoder(forKey :)
為超類準備編碼器。 將以下代碼添加到playground
的末尾以測試您的可編碼子類:
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
的值以查看您的工作! 您可以在應用程序中處理更復雜的類層次結構。 是時候進行下一次挑戰!
Handling Arrays With Mixed Types
Gifts
部門API公開了適用于不同類型員工的JSON:
[
{
"name" : "John Appleseed",
"id" : 7
},
{
"id" : 7,
"name" : "John Appleseed",
"birthday" : 580797832.94787002,
"toy" : {
"name" : "Teddy Bear"
}
}
]
此JSON數組是多態的,因為它包含默認和自定義employees
。 打開Polymorphic types
,您將看到不同類型的員工由枚舉表示。 首先,聲明枚舉是Encodable
:
enum AnyEmployee: Encodable {
然后將此代碼添加到枚舉的主體:
// 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)
}
}
以下是此代碼的用途:
- 1) 定義足夠的編碼密鑰以涵蓋所有可能的情況。
- 2) 對有效員工進行編碼,并對無效員工拋出
EncodingError.invalidValue(_:_ :)
。
通過將以下內容添加到playground
的末尾來測試您的編碼:
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)!
檢查employeesString
的值以查看混合數組。
解碼有點復雜,因為在決定如何繼續之前,你必須弄清楚JSON中的內容。 將以下代碼添加到playground
:
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
}
}
}
// 4
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData)
這就是它的工作原理:
- 1) 像往常一樣獲取一個鍵控容器,然后檢查
allKeys
屬性以確定JSON中存在哪些鍵。 - 2) 檢查
containerKeys
是否與默認員工或自定義員工所需的密鑰匹配,并提取相關屬性;否則,建立一個.noEmployee
。 如果沒有合適的默認值,您可以選擇在此處拋出錯誤。 - 3) 將
employeesData
解碼為[AnyEmployee]
。
您可以根據具體類型對employeesData
中的每個employee
進行解碼,就像編碼一樣。
只留下兩個挑戰 - 下一個挑戰!
Working With Arrays
禮品部門為員工的生日禮物添加標簽;他們的JSON看起來像這樣:
[
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
JSON
數組包含小寫,大寫和常規標簽名稱。 這次你不需要任何密鑰,所以你使用一個unkeyed container
。
打開Unkeyed container
容器并將編碼代碼添加到Label
:
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
就像你目前使用的容器一樣工作,除了......你猜對了,沒有鍵。 可以將其視為寫入JSON數組而不是JSON字典。 您將三個不同的字符串編碼到容器中。
運行playground
并檢查labelString
以查看您的數組。
這是解碼的外觀。 將以下代碼添加到playground
的末尾:
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)
}
}
// 2
let sameLabel = try decoder.decode(Label.self, from: labelData)
這就是上面代碼的工作原理:
- 1) 獲取解碼器的無鍵解碼容器
(unkeyed decoding container)
,并使用decode(_ :)
循環解碼,以解碼最終的,格式正確的標簽名稱。 - 2) 使用未加密碼的解碼容器
(unkeyed decoding container)
將labelData
解碼為Label
。
您遍歷整個解碼容器,因為最后會出現正確的標簽名稱。
你最后一次挑戰的時間!
Working With Arrays Within Objects
禮品部門想要查看員工生日禮物的名稱和標簽,因此其API生成的JSON如下所示:
{
"name" : "Teddy Bear",
"label" : [
"teddy bear",
"TEDDY BEAR",
"Teddy Bear"
]
}
您將標簽名稱嵌套在label
內。 與前一個挑戰相比,JSON結構增加了額外的縮進級別,因此在這種情況下,您需要使用嵌套的無鍵容器(nested unkeyed containers)
作為label
。
打開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)
}
在這里,您將創建一個嵌套的無鍵容器,并使用三個標簽值填充它。 運行playground
并檢查string
以檢查結構是否正確。
如果JSON具有更多縮進級別,則可以使用更多嵌套容器。 將解碼代碼添加到playground
頁面:
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)
這遵循與以前相同的模式,通過數組并使用最終值來設置label
的值,但是來自嵌套的未鍵控容器(nested unkeyed container)
。
恭喜您完成所有挑戰!
后記
本篇主要講述了Swift編碼和解碼,感興趣的給個贊或者關注~~~