译自 Cupcake Corner: Introduction
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
在这个项目中,我们会构建一个用于订购蛋糕的多屏应用。这会用到几个表单,而表单对你来说已经不新鲜了。但是,你还将学到如何让类在它具有 @Published
属性时遵循 Codable
,如何通过网络发送和接收定单数据,以及如何验证表单,等等。
随着我们持续深入 Codable
,我希望你会继续对它的灵活性和安全性印象深刻。特别是,我希望你记住它和较老的 UserDefaults
API有很大的不同 —— 不用精确地输入字符串这一点真是太好了!
言归正传,我们有很多工作要做,让我们开始吧:使用 Single View App 模板创建一个新的 iOS 应用,取名为 CupcakeCorner。
与往常一样,我们从这个项目会用到的新技术开始。
译自Adding Codable conformance for @Published properties
如果一个类型的所有属性都已经遵循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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~