如果类型的所有属性都已经符合Codable
,则类型本身就可以满足Codable
协议,无需额外的工作——Swift将根据需要以合成归档和解档的代码。但是,当我们使用属性包装器时(如@Published
),此方法不起作用,这意味着要符合Codable
协议需要我们进行一些额外的工作。
要解决此问题,我们需要自己实现Codable
。这将解决@Published
归档问题,这在其他地方也是一项宝贵的技能,因为它使我们能够精确控制要保存的数据以及数据的存储方式。
首先,让我们创建一个简单的类型来重现问题。将该类添加到ContentView.swift
中:
class User: ObservableObject, Codable {
var name = "Paul Hudson"
}
那样编译就可以了,因为String
符合Codable
要求。但是,如果我们这样做,@Published
则代码将不再编译:
class User: ObservableObject, Codable {
@Published var name = "Paul Hudson"
}
该@Published
属性包装器不是魔术——属性包装器来自于一个事实:我们的name
属性会自动包装在另一个类型中,该类型会添加一些其他功能。在这种情况下,可以存储任何类型的值的结构@Published
被称为Published
。
以前,我们研究了如何编写适用于任何类型值的通用方法,而Published
结构又迈出了一步:整个类型本身都是通用的,这意味着您不能自己创建Published
的实例,而是创建一个Published
的实例——包含字符串的可发布对象。
如果这听起来令人困惑,请回想一下:这实际上是Swift的一项基本原则,并且您已经使用了一段时间。考虑一下-我们不能说var names: Set
,可以吗?Swift不允许这样做;Swift 想知道是什么东西存储在Set
内。这是因为Set
也是通用类型:您必须创建Set
的实例。数组和字典也是如此:我们总是使它们内部具有特定的内容。
Swift已经制定了规则,说如果数组包含符合Codable
协议的类型,则整个数组符合Codable
协议,字典和集合也相同。但是,SwiftUI 并未为其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
实例请求一个容器,该容器通过 decoder.container(keyedBy: CodingKeys.self)
与我们在CodingKeys
中设置的所有编码键匹配。这意味着“此数据应有一个容器,其中的键与CodingKeys
枚举中的大小写匹配。这是一个可抛出异常的调用,因为这些键可能不存在。
最后,我们可以通过引用枚举中的 case
直接从该容器读取值—— container.decode(String.self, forKey: .name)
。这通过两种方式提供了非常强大的安全性:我们明确表示希望读取字符串,因此,如果name
将其更改为整数,则代码将停止编译;而且我们还在CodingKeys
枚举中使用了case
写而不是字符串,因此没有错别字的机会。
在该User
类符合Codable
之前,我们还需要完成另一项任务:我们已经创建了一个初始化器,以便Swift可以将数据解码为这种类型,但是现在我们需要告诉Swift如何对这种类型进行编码 —— 如何将其归档以备编写JSON。
此步骤几乎与我们刚才编写的初始化程序相反:我们将Encoder
实例传入,要求它使用我们的CodingKeys
枚举作为键来创建一个容器,然后将我们的值附加到每个键上。
现在将此方法添加到User
类中:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
现在,我们的代码开始编译:Swift知道我们要写入什么数据,知道如何将一些编码数据转换为对象的属性,并且知道如何将对象的属性转换为某些编码的数据。
希望您能在这里看到一些与UserDefaults
的字符串型API相比的真正优势——Codable
会更难以出错,因为我们不使用字符串,并且它会自动检查我们的数据类型是否正确。
译自 Adding Codable conformance for @Published properties
里程碑:项目 7 - 9 | Hacking with iOS: SwiftUI Edition | SwiftUI:使用 URLSession 发送和接收 Codable 数据 |
---|
赏我一个赞吧~~~