[SwiftUI 100 天] Cupcake Corner - part1

貓克杯發表於2020-04-06
譯自 Cupcake Corner: Introduction
更多內容,歡迎關注公眾號 「Swift花園」
喜歡文章?不如來個 ??➕三連?關注專欄,關注我 ???

Cupcake Corner:介紹

在這個專案中,我們會構建一個用於訂購蛋糕的多屏應用。這會用到幾個表單,而表單對你來說已經不新鮮了。但是,你還將學到如何讓類在它具有 @Published 屬性時遵循 Codable,如何通過網路傳送和接收定單資料,以及如何驗證表單,等等。

隨著我們持續深入 Codable,我希望你會繼續對它的靈活性和安全性印象深刻。特別是,我希望你記住它和較老的 UserDefaults API有很大的不同 —— 不用精確地輸入字串這一點真是太好了!

言歸正傳,我們有很多工作要做,讓我們開始吧:使用 Single View App 模板建立一個新的 iOS 應用,取名為 CupcakeCorner。

與往常一樣,我們從這個專案會用到的新技術開始。


譯自Adding Codable conformance for @Published properties

為 @Published 屬性新增 Codable 實現

如果一個型別的所有屬性都已經遵循Codable,那麼型別本身就可以遵循Codable,無需進行額外的工作 —— Swift 會根據需要合成用於歸檔和解檔你的型別的程式碼。但是,當我們使用@Published之類的屬性包裝器時,這就不管用了,意味著遵循Codable需要我們做一些額外的工作。

class User: ObservableObject, Codable {
   var name = "Paul Hudson"
}複製程式碼

上面的程式碼不會有編譯問題,因為String本來就遵循Codable。但是,如果我們把name標記為@Published,那麼程式碼將通不過編譯:

class User: ObservableObject, Codable {
    @Published var name = "Paul Hudson"
}複製程式碼

@Published屬性包裝器並不是魔術,“屬性包裝器”這個名稱來源於一個事實,我們的name屬性被自動地包裝在另一種型別中,這個型別增加了一些附加的功能。對於@Published,這是一個名叫Published的結構體,可以儲存任何型別的值。

之前,我們研究瞭如何編寫適用於任何型別的泛型方法,而Published結構體比那更進一步:整個型別本身就是泛型,這意味著你無法只通過Published本身建立一個例項,而只能通過像建立一個包含字串的 published 物件這樣的方式來建立Published例項。

如果這聽起來令人困惑,請做好筆記:這實際上是一個相當基礎的 Swift 原理,而且你已經使用了一段時間。思考一下 —— 我們不能說var names:Set,可以嗎?Swift 不允許這樣做;Swift 要知道集合中有什麼。這是因為Set是一個泛型:你必須建立一個Set的例項。陣列和字典也是如此:我們總是使它們的內部具有特定的內容。

Swift 已經制定了規則,如果陣列包含Codable型別,那麼整個陣列就是Codable的,字典和集合也是如此。但是,SwiftUI 並沒有為Published結構體提供相同的功能 —— 它沒有規定說“如果 published 物件是Codable的,那麼Published結構體本身也是Codable的”。

因此,我們需要自己實現這些型別(遵循Codable):我們需要告訴 Swift 應該載入和儲存哪些屬性,以及如何執行這兩項操作。

這些步驟都不是很難的,所以讓我們開始第一個步驟:告訴 Swift 應該載入和儲存哪些屬性。這是通過用一個列舉實現特殊協議CodingKey來完成的,列舉中的每種情況都是我們要載入和儲存的屬性的名稱。簡單起見,這個列舉通常就叫CodingKeys,在末尾帶有 s ,但如果你想給它起別的名字也沒問題。

因此,我們的第一步是建立一個遵循 `CodingKey` 的 `CodingKeys` 列舉,列出我們要歸檔和解檔的所有屬性。現在,把下面的程式碼新增到 `User` 類中:

enum CodingKeys: CodingKey {
    case name
}複製程式碼

下一個任務是建立一個自定義的構造器,這個構造器會被賦予某種容器,我們並用這種容器來讀取所有屬性的值。這會涉及一些新的知識,不如讓我們先看一下程式碼 —— 把這個構造器新增到User

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

即使程式碼不多,也至少有四樣新東西。

首先,這個構造器被傳入了一個新的型別,叫 Decoder。它包含了我們所有的資料,而我們需要弄清楚如何讀取它們。

其次,任何繼承我們的User類的人都必須用一個自定義實現來重寫這個構造器,以確保他們新增自己的值。我們用required關鍵字標記構造器:required init。有一種替代方法是將這個類標記為final,以便不允許子類化,在這種情況下,我們要寫成final class User,然後完全刪除required關鍵字。

再次,在方法內部,我們通過decoder.container(keyedBy: CodingKeys.self)Decoder例項請求一個跟我們在CodingKey結構體中設定的所有編碼鍵匹配的容器。它的意思是 “這份資料應該具有一個容器,其中的鍵與我們在CodingKeys列舉中列出的鍵相匹配” 。這是一個會丟擲錯誤的呼叫,因為這些鍵可能不存在。

最後,我們可以通過引用列舉中的 case 直接從容器中讀取值:container.decode(String.self, forKey: .name)。這裡通過兩種方式提供了非常強大的安全性:我們明確表示希望讀取的是字串,因此,如果把name改為整數,那麼程式碼會停止編譯;並且,我們是使用CodingKeys列舉中的 case 而不是字串,因此沒有拼錯字串的風險。

為了讓User類實現Codable,我們還需要完成另一項任務:我們已經建立了一個構造器,以便 Swift 可以將資料解碼User,但是現在我們需要告訴 Swift 如何編碼User—— 如何將其歸檔以便寫入 JSON 。

這個步驟跟剛剛編寫的構造器的過程幾乎相反:我們被傳入一個用於寫入的Encoder例項,我們管它要一個以CodingKeys列舉作為鍵的容器,然後依照鍵把每個值寫入容器。

這個步驟跟剛剛編寫的構造器的過程幾乎相反:我們被傳入一個用於寫入的Encoder例項,我們管它要一個以CodingKeys列舉作為鍵的容器,然後依照鍵把每個值寫入容器。

現在,把這個方法新增到User類:

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
}複製程式碼

現在,我們的程式碼可以編譯通過了:Swift 知道我們要寫入的資料,知道如何將某些編碼的資料轉換為物件的屬性,還知道如何將物件的屬性轉換為某些編碼的資料。

希望你能在這裡面看到相對 UserDefaults 的 “stringly 型別的” API 的真正優勢 —— 由於我們不使用字串,用 Codable 的話,想要出錯要難得多,因為它會自動檢查我們的資料型別是否正確。

我的公眾號 這裡有Swift及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~

[SwiftUI 100 天] Cupcake Corner - part1

???????????????????????


相關文章