Swift — 說說 Codable ( Decodable & Encodable)

利用 Swift 中的 Codable 協議來方便編/解碼資料。

Jeremy Xue
10 min readAug 13, 2018
Codable

前言:

常常我們在開發 APP 中會有一個需要串接網路上資料的需求,最常見的需求可能就是解析API 或是 JSON 類型的檔案,因此我們常常需要把 JSON 中的資料轉化成我們所需的資料。在 Swift 4 的時候,帶來了一個強大的特性 —— Codable 能夠將 JSON 檔轉換成我們所定義的架構,而不需要變得像是字典去慢慢解析,現在就讓我們來看一下什麼是 Codable 吧。

Codable

一個可以轉換自身結構為外部表示的類型。

Codable 是 Encodable 和 Decodable 協議的類型別名。當你使用 Codable 作為類型或泛型的約束時,它將同時遵循兩種協議的任何類型。

typealias Codable = Decodable & Encodable

Encoding and Decoding Custom Types

使你的資料類型可編碼( encodable )和可解碼 ( decodable ),用於與外部表示( 例如:JSON )相容。

Swift 的標準庫定義了編碼和解碼的標準方式。你可以透過在自定義的類型上實現 Encodable 和 Decodable 協議來使用此方法。採用這些協議允許 Encoder 和 Decoder 協議的實現獲取的數據,並將其編碼或解碼到外部表示或從外部表示解碼( 如 JSON 或是屬性列表( PropertyList ))。

為了支持編碼和解碼兩者,聲明與 Codable 的一致性,Codable 結合了 Encodable 和 Decodable 協議。這個過程成為使你的類型可編/解碼( codable )。

Encode and Decode Automatically

使類型可編碼最簡單的方式是使用已經可編碼的類型聲明其屬性。這些類型包括標準庫的類型,如 String、Int 和 Double 以及基礎類型,如 Date、Data 和 URL。任何類型的屬性都自動可編碼的類型只需透過聲明其一致性即可符合 Codable。

創建一個 Landmark 結構,它存儲地標的名稱和創始年份:

struct Landmark {
var name: String
var foundingYear: Int
}

將 Landmark 的繼承 Codable 會觸發滿足 Encodable 和 Decodable 的所有協議要求的自動一致性:

struct Landmark: Codable {
var name: String
var foundingYear: Int

// Landmark 現在支持 Codable 中的方法 init(from:) 和 encode(to:),
// 即便它們沒有被寫為宣告的一部分。
}

採用 Codable 在你自己的類型上使你將他們轉換為任何內建數據格式,以及自訂的 Encoder 和 Decoder 提供的任何格式。例如,Landmark 結構可以使用 PropertyListEncoder 和 JSONEncoder 類進行編碼,即使 Landmark 本身不包含專門處理屬性列表或 JSON 的代碼。

同樣的原理適用於由可編/解碼自定義的類型組成的自定義類型。只要它的所有屬性都是 Codable,任何自定義類型也可以是 Codable。

下面的範例演示將座標屬性( Coordinate )添加到 Landmark 結構時如何應用保持 Codable 自動一致性:

struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}
struct Landmark: Codable {
// Double、String、Int 都符合 Codable。
var name: String
var foundingYear: Int

// 添加一個自定義的 Codable 類型屬性可保持整體 Codable 的一致性。
var location: Coordinate
}

內建類型,如 Array、Dictionary 和 Optional 在包含可編/解碼類型時也符合 Codable。你可以向 Landmark 添加一個 Coordinate 實例 Array,整理結構仍然滿足 Codable。

下面的範例演示了在 Landmark 中使用內建可編/解碼類型添加多個屬性時,自動一致性如何仍然適用:

struct Landmark: Codable {
var name: String
var foundingYear: Int
var location: Coordinate

// 添加這些屬性後,Landmark 仍然為 Codable。
var vantagePoints: [Coordinate]
var metadata: [String: String]
var website: URL?
}

Encode or Decode Exclusively

在某些情況下,你可能不需要 Codable 支持雙向的編碼或是解碼。例如,某些 App 只需要調用遠端網路的 API 而不需要解碼包含同類型的響應。如果你只需要支持數據編碼,則宣告符合 Encodable。相反,如果你只需要讀取給定類型的數據,則宣告符合 Decodable,就是根據 App 的需求來決定。

以下範例演示了僅對數據進行編碼或解碼的 Landmark 結構的宣告:

struct Landmark: Encodable {
var name: String
var foundingYear: Int
}
struct Landmark: Decodable {
var name: String
var foundingYear: Int
}

Choose Properties to Encode and Decode Using Coding Keys

Codable 類型可以宣告一個名為 CodingKeys 特殊的嵌套枚舉,它符合 CodingKey 的協議。當這個枚舉存在時,其情況作用於編碼或解碼可編/解碼類型的實例時必須包含的屬性的權威列表。

如果解碼實例時他們不存在,或者如果某些屬性不應該包含在編碼表示中,則忽略 CodingKey 枚舉中的屬性。CodingKeys 中忽略的屬性需要一個默認值,以使其包含類型能夠接收與 Decodable 或 Codable 的自動一致性。

如果串接數據格式中使用的 key 與數據類型中的屬性名稱不匹配,請透過將 String 指定為 CodingKeys 枚舉的原始類型來提供備用的 key。用作每個枚舉情況的原始值的字串是在編碼和解碼期間使用的 key name。案例名稱與其原始值之間的關聯使你可以根據 Swift API 設計指南 命名數據,而不必匹配正在建模時的串接格式的名稱,標點符號和大小寫。

以下示例在編碼和解碼時使用替代鍵作為 Landmark 結構的 name 和 foundingYear 屬性:

struct Landmark: Codable {
var name: String
var foundingYear: Int
var location: Coordinate
var vantagePoints: [Coordinate]

enum CodingKeys: String, CodingKey {
case name = "title"
case foundingYear = "founding_date"

case location
case vantagePoints
}
}

Encode and Decode Manually

如果 Swift 類型的結構與其編碼形式結構不同,則可以自己提供 Encodable 和 Decodable 的自定義實現來定義自己的編碼和解碼邏輯。

在下面範例中,擴展了 Coordinate 結構來支持嵌套在 additionalInfo 中的 elevation 屬性:

struct Coordinate {
var latitude: Double
var longitude: Double
var elevation: Double
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case additionalInfo
}

enum AdditionalInfoKeys: String, CodingKey {
case elevation
}
}

因為 Coordinate 類型的編碼形式包含第二級嵌套訊息,所以類型採用 Encodable 和 Decodable 協議使用兩個枚舉,每個枚舉列出在特定級別上使用的完整編碼 Key。

在下面的範例中,通過實現其所需的初始化程序 init(from :),擴展了 Coordinate 結構以符合 Decodable 協議:

extension Coordinate: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)

let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
}
}

初始化程序通過使用它作為參數接收的 Decoder 實例上的方法來填充 Coordinate 實例。 Coordinate 實例的兩個屬性使用 Swift 標準庫提供的鍵控容器 API 進行初始化。

下面的範例演示了如何通過實現其所需的方法 encode(to :) 來擴展Coordinate結構以符合 Encodable 協議:

extension Coordinate: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)

var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
try additionalInfo.encode(elevation, forKey: .elevation)
}
}

encode(to :) 方法的這種實現前一個例子相反的解碼操作。

後記:

這篇文章只是單純將官方文檔上面的概論及範例翻成中文,因為查了很多有關 Codable 的用法及資料,每個人的做法不同,因此選擇了最標準的官方文件來介紹,肯定不會有太大的問題吧 😂😂😂,下一次我們將簡單的使用 Codable 方式來解析我們的 API ,玩玩 Codable 。

--

--

Jeremy Xue

Hi, I’m Jeremy. [好想工作室 — iOS Developer]