iOS 學習使用 Swift Codable

New丶Life發表於2018-11-25

Codable 是swift4的新特性, 使用其可以用更少的程式碼更快的實現資料編解碼

JSON

一種基於文字的用於表示資訊的格式,人類和計算機都很容易讀寫這種格式,也是目前使用最廣泛的標準

簡單寫一個json文字

let json = """
{
    "name" : "xiaoming",
    "age" : 18,
    "score" : 99.5

}
""".data(using: .utf8)!
複製程式碼

我們用上面的json來模擬網路資料請求得到的結果,下面構建model和解析

構建模型

我們建立一種適合這種json表示的swift型別 也就是model, 讓json中的值和swift系統中定義型別互相匹配。

  • JSON中的物件是無序鍵值對,和swift中包含 StringDictionary 類似
  • JSON 將數字表示成一系列數,不受任何語義的影響,它不區分整數和浮點數,不區分定長和非定長數字,也不區分是二進位制還是十進位制。每個實現自行決定如何解釋這些數字。
  • 對於具有 Optional 屬性的型別, Codable 可以自動將 null 對映為 nil。
struct Student : Codable {
    var name : String
    var age : Int
    var score : Double
}
複製程式碼

我們首先定義一個結構體 Student 來對應資料中的頂層物件, 這裡我們使用結構體,使用 Class 也無妨。

Codable 協議簡介

上面的結構體 Student 遵循了 Codable 協議, 其大大提升了物件和其表示之間相互轉換的體驗。

理解 Codable 最好的方式就是看它的定義:

typealias Codable = Decodable & Encodable

Codable 是一種 混合型別,由 DecodableEncodable 協議構成。

Decodable 協議定義了一個初始化函式:

init(from decoder: Decoder) throws

遵從 Decodable 協議的型別可以使用任何 Decoder 物件進行初始化。

Encodable 協議定義了一個方法:

func encode(to encoder: Encoder) throws

任何 Encoder 物件都可以建立遵從了 Encodable 協議型別的表示。

只有當一個型別滿足這個協議的所有要求的時候,我們才能說這個型別 遵從 那個協議了。 對於 Decodable 而言,唯一的要求就是 init(from:) 初始化方法。

init(from:) 初始化方法接受一個 Decoder 引數。 Decoder 協議需要闡明將 Decodable 物件的表示解碼成物件的要求。為了適應各種資料交換格式,解碼器和編碼器都使用名為 容器(container)的抽象。容器是用來儲存值的,可以儲存一個值也可以儲存多個值,可以像字典一樣有鍵去對應值,也可以像陣列一樣不需要鍵。...

因為上面那個 JSON 表示的頂層有一個物件, 所以我們就建立一個有鍵的容器,然後用不同的鍵來解碼各個屬性。

我們建立一個 CodingKeys 列舉,定義屬性名稱和容器的鍵之間的對映。這個列舉宣告其原始型別是 String,同時宣告使用 CodingKey 協議。因為每個名字都和 JSON 中的鍵相同,所以我們不用為這個列舉提供明確的原始值。

struct Student: Decodable {
    // ...

    private enum CodingKeys: String, CodingKey {
        case name
        case age
        case score
    }
}...
複製程式碼

接下來,在 init(from:) 初始化函式裡我們建立一個鍵控容器,呼叫decodercontainer(keyedBy:) 方法,並傳入 CodingKeys 作為引數。

最後,我們呼叫 containerdecode(_:,forKey:) 方法把每個屬性初始化一下。

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    self.age = try container.decode(Int.self, forKey: .age)
    self.score = try container.decode(Double.self, forKey: .score)
}...
複製程式碼

我們有了一個 Student 模型,也遵從了 Decodable 協議,現在我們可以從 JSON 表示中建立一個 student 物件了

把 JSON 解碼成模型物件

先引用 Foundation,以便使用 JSONDecoderJSONEncoder

import Foundation

建立一個 JSONDecoder 物件並呼叫其 decode(_:from:) 方法

let decoder = JSONDecoder()
let student = try! decoder.decode(Student.self, from: json)
複製程式碼

試驗轉換成功與否

print(student.name) 
print(student.age) 
print(student.score) 
複製程式碼

將模型物件編碼為 JSON

下面我們實現 Encodable 協議要求的 encode(to:) 方法。 encode(to:)init(from:) 就像一對映象一樣工作。 先呼叫 encodercontainer(keyedBy:) 方法建立一個 container, 和上一步一樣,傳入一個 CodingKeys.self 引數。 這裡我們把 container 當作一個變數(使用 var 而不是 let), 因為這個方法做的是填充 encoder 的引數,需要進行修改。我們對每個屬性呼叫一次 encode(_:forKey:) 並傳入屬性的值和它對應的鍵。...

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.name, forKey: .name)
    try container.encode(self.age, forKey: .age)
    try container.encode(self.score, forKey: .score)
}...

複製程式碼
let encoder = JSONEncoder()
let reencodedJSON = try! encoder.encode(stuent)

print(String(data: reencodedJSON, encoding: .utf8)!)...

複製程式碼

JSON 對空格和空行(whitespace)不敏感,但是你可能會在意。如果你覺得輸出不美觀,把 JSONEncoderoutputFormatting 屬性設定為 .prettyPrinted 就好了

刪除無用的程式碼

把所有我們寫的 Codable 的實現都刪掉。這樣,應該就只剩下結構和屬性的定義了。

struct Student : Codable {
    var name : String
    var age : Int
    var score : Double
}
複製程式碼

直接執行程式碼,試試編碼和解碼 JSON。有什麼變化嗎?完全沒有。 這讓我們看到了 Codable 的殺手級特性:

Swift 自動整合了 DecodableEncodable 的一致性。

一種型別只要在其宣告中採用協議即可,也就是說,不用在擴充套件中再做額外的事,只要它的每個屬性都有一個符合其協議,其他一切都自動完成了。

嘗試解析各種格式的JSON文字

JSON頂層是陣列的情況:

    let jsonModels = """
    [
        {
            "name" : "xiaoming",
            "age" : 18,
            "score" : 99.5
        },
        {
            "name" : "daxiong",
            "age" : 19,
            "score" : 66
        }
    ]
    """.data(using: .utf8)!
複製程式碼

把它解析成 Student 物件的陣列十分簡單:像之前一樣呼叫 decode(_:from:),把 Student.self 換成 [Student].self 以便去解析 Student 陣列而不是單個物件。

let students = try! decoder.decode([Student].self, from: jsonModels)

為什麼這麼簡單就可以了?這要歸功於 Swift 4 的另一個新特性:條件一致性。

// swift/stdlib/public/core/Codable.swift.gyb
extension Array : Decodable where Element : Decodable {
    // ...
}

複製程式碼

我們再在陣列的外層套一個字典的形式:


    let jsonComplex = """
    {
        "students" : [
            {
                "name" : "xiaoming",
                "age" : 18,
                "score" : 99.5
            },
            {
                "name" : "daxiong",
                "age" : 19,
                "score" : 66
            }
        ]
    }
    """.data(using: .utf8)!

複製程式碼

如果 ArrayDictionary 包含的 KeyTypeValueType 都遵從 Decodable 協議,那麼這樣的陣列或字典本身也就遵從了 Decodable 協議:

// swift/stdlib/public/core/Codable.swift.gyb
extension Dictionary : Decodable where Key : Decodable, 
                                       Value : Decodable {
    // ...
}...
複製程式碼

因此你可以把之前 decode(_:from:) 的呼叫引數直接換成 [String: [Student]].self (Dictionary<String, Plane> 的簡寫):

        let jsonData = try! decoder.decode([String: [Student]].self, from: jsonComplex)
        let students = jsonData["students"]
複製程式碼

或者你也可以建立一個遵從 Decodable 協議的新型別,然後給這個型別一個屬性來放 [Student] ,該屬性的名字應和 JSON 中的鍵名相對應,再把這個新型別傳入 decode(_:from:) 函式中:

struct Brige: Decodable {
    var students: [Student]
}

let Brige = try! decoder.decode(Brige.self, from: json)
let students = Brige.students...
複製程式碼
  • Codable 是一種 混合型別,由 DecodableEncodable 協議構成。
  • 只要一種型別中的每個屬性都遵從協議,在型別定義時宣告一下,Swift 就會自動整合 DecodableEncodable
  • 如果 ArrayDictionary 中的每一個元素都遵從 Codable 型別,那麼它們本身也會遵從 Codable

相關文章