SwiftUI:为 @Published 属性添加 Codable 支持

如果类型的所有属性都已经符合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,但是如果需要,您可以将其称为其他名称。

因此,我们的第一步是创建符合CodingKeyCodingKeys枚举,,列出我们要归档和解归档的所有属性。现在将其添加到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 数据

赏我一个赞吧~~~

你可能感兴趣的:(SwiftUI:为 @Published 属性添加 Codable 支持)