[SwiftUI 100 天] Cupcake Corner - part1

646 阅读6分钟
译自 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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~

                                                                   

􏰲􏱓􏵠􏱂􏳊􏶄􏲌􏵘􏱢􏰿􏲧􏶃􏴨􏲋􏱽􏴑􏰚􏲝􏶢􏶣􏴅􏲤􏶊