From b0473bee790a824df6107df436877dc41db36301 Mon Sep 17 00:00:00 2001 From: Joeytat Date: Fri, 20 Dec 2019 00:30:29 +0800 Subject: [PATCH 01/20] add swift-protocols-app-configuration --- ...91220_swift-protocols-app-configuration.md | 608 ++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 appcoda/20191220_swift-protocols-app-configuration.md diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md new file mode 100644 index 00000000..fc68c577 --- /dev/null +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -0,0 +1,608 @@ +title: "如何利用 Swift 协议来管理应用配置" +date: 2019-12-20 +tags: [教程] +categories: [AppCoda] +permalink: swift-protocols-app-configuration +keywords: 协议,应用配置 +custom_title: 如何利用 Swift 协议来管理应用配置 +description: 本文详细讲解了如何利用 Swift 协议来管理应用配置。 + +--- +原文链接=https://www.appcoda.com/swift-protocols-app-configuration/ +作者=Gabriel Theodoropoulos +原文日期=2019-08-30 +译者=Joeytat +校对= +定稿= + + +大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我不认为有任何一个程序员不知道协议。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: + +> 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是满足了该协议。 + + +用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 + +让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为一个 Swift 开发中的流行话题的原因。通过定义一组方法来描述一系列功能,并对他们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 + +如果你是一个新手开发者,那么我强烈推荐你阅读[**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 + +好了,那到底协议与这篇文章标题所说的应用的设置有什么关系吗?让我来解释一下,并且将其建立起联系。很久以来,我一直都被 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用他们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! + +# 线路图 +我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。我们将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,再到最后的一个指令,就会让它完全正常运转。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来我们将要创建的文件是 *属性列表*(.plist)。毕竟当我们考虑设置时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们让要加上对应用包中的默认初始设置的处理支持(那些我们可以在 Xcode 属性列表编辑器中编辑的设置)。 + +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里我们是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 + +最后,这里有一个供你下载的[启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用来放在协议中改变。在你下载之后,请用 Xcode 打开然后继续往下。 + +# 开始:创建协议 +让我们打开启动项目,然后创建一个将用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 + +下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 Create 按钮,来让 Xcode 真正地创建新文件。 + +> 注意: 这篇文章中的代码是在 Xcode 10.3 中创建的,这是在撰写这篇文章时最新且最稳定的 Xcode 版本。 + +当新文件准备好时,去 `Project Navigator` 选中并且打开它。协议的定义是: + +```swift +protocol SettingsManageable { + +} +``` + +我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样我们可以通过采纳协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: + +```swift +extension SettingsManageable where Self: Codable { + +} +``` + +注意我们在这里设置的条件。通过扩展尾部的 `where Self: Codable` 条件,我们要求了任何自定义类型在采纳 `SettingsManageable` 协议的同时,也要采纳 [`Codable`](https://developer.apple.com/documentation/swift/codable) 协议。 + +为什么我们需要 `Codable`?因为我们会对采纳了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 + +以防止有的类型并未采纳 `Codable` 但采纳了 `SettingsManageable`,这样就别指望我们接下来能够为其提供正确的功能了。 + +# 定义并且实现协议的要求 +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何采纳了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! + +## 获取文件 URL +任何采纳了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! + +向协议中添加下面方法的定义: + +```swift +protocol SettingsManageable { + func settingsURL() -> URL +} +``` + +在协议扩展中我们对其实现: + +```swift +extension SettingsManageable where Self: Codable { + func settingsURL() -> URL { + let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + return cachesDirectory.appendingPathComponent("\(Self.self).plist") + } +} +``` + +第一行我们从缓存目录获取到了 URL。第二行我们将*采纳了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 + +注意 `\(Self.self)` 动态地提供了采纳 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类采纳了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体采纳了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 + +## 存储(更新)一个 SettingsManageable 实例 +毋庸置疑我们上面实现的方法是非常实用的,但是它也只是一个为了我们方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将采纳协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: + + 1. 编码为属性列表 + 2. 写入文件 + +再次回到 `SettingsManageable` 协议中,添加下面的定义: + +```swift +protocol SettingsManageable { + ... + func update() -> Bool +} +``` + +我们将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 + +我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 对象转化成 JSON。有很大概率你已经用过了它!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? + +噢,是的,就是有这么一个类,而且我们将要用上它!在协议扩展中,先写上如下代码: + +```swift +func update() -> Bool { + do { + let encoded = try PropertyListEncoder().encode(self) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先我们创建了一个 `PropertyListEncoder` 的类,然后我们调用了 `encode(_:)` 方法来对 `self`(采纳了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! + +如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里我们并未对异常做任何额外操作,我们只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 + +上面的代码并没有将编码后的数据*写入*文件。在 `do` 中,再加上这么一行: + +```swift +do { + let encoded = try PropertyListEncoder().encode(self) + try encoded.write(to: settingsURL()) + return true +} +``` + +`Data` 类中的 `write(to:)` 方法同样会抛出异常, 所以配合 `try` 标记使用也是强制要求的。注意在这里我们就用上了之前实现的获取文件 URL 的 `settingsURL()` 方法。 + +只通过两行重要的代码,我们就实现了编码和将目标类型写入属性列表文件的功能。整个方法应该像这样: + +```swift +func update() -> Bool { + do { + let encoded = try PropertyListEncoder().encode(self) + try encoded.write(to: settingsURL()) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望我们的方法内部使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: + +```swift +func update() throws +``` + +然后在协议扩展中这样修改: + +```swift +func update() throws { + let encoded = try PropertyListEncoder().encode(self) + try encoded.write(to: settingsURL()) +} +``` + +可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了方便你,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 + +> 注意:想要了解更多关于错误处理的内容?看看[这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)吧! + +## 通过文件加载设置 +现在让我们过渡到通过文件加载数据,我们刚刚提到的保存数据的对应部分。我们要跟随的步骤很简单,所有要做的就以下几点: + +1. 检查文件是否存在。 +2. 将文件内容加载为 `Data` 对象。 +3. 通过 **`PropertyListDecoder`** 类来对其解码,与 `PropertyListEncoder` 相对应。 + +和我们之前做的相似,这个方法也会返回一个布尔值来表示成功与否。在 `SettingsManageable` 协议中添加下面的内容: + +```swift +protocol SettingsManageable { + ... + mutating func load() -> Bool +} +``` + +这个方法被标记为*可变*,因为它会修改采纳了 `SettingsManageable` 协议的类型的实例(简单来说,它会修改 `self`)。 + +这个方法的实现其实很简单并且和我们之前做的很相似。只是额外增加了对文件是否存在的检查。在协议扩展的方法中添加如下实现: + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + do { + let fileContents = try Data(contentsOf: settingsURL()) + self = try PropertyListDecoder().decode(Self.self, from: fileContents) + return true + } catch { + print(error.localizedDescription) + return false + } + } +} +``` + +我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次我们需要使用 `path` 属性来获取路径字符串。当我们确定文件存在之后,我们就从文件中加载内容到 `fileContents` 实例中,然后我们就拿到加载的数据,通过 `PropertyListDecoder` 类解码后,对 `self`(采纳了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,我们就返回 true,如果出现了错误则会抛出异常,然后我们会将错误打印在控制台并且返回 false。 + +现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况我们还没有返回值。如果没有文件可以加载设置,那么我们就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + ... + } else { + return update() + } +} +``` + +现在方法在所有的情况下都有返回值了。如果我们第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 + +## 通过应用包中的 Plist 文件初始化设置 +既然我们一直在聊设置,那么很自然就会想到他们的的默认值。在编程层面,采纳了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者唯一适合的方式。通过 Xcode 的属性列表编辑器来编辑放在应用包中的属性列表文件,以初始化属性列表文件或许是更好也跟简单的方式。我们将要在这个部分覆盖这种情况,然后还有**一条很重要的规则**需要继续: + +*属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* + +否则的话,解码会失败。 + +让我们通过在 `SettingsManageable` 协议中定义一个新的方法开始吧: + +```swift +protocol SettingsManageable { + ... + + mutating func loadUsingSettingsFile() -> Bool +} +``` + +在这个方法的实现中(协议扩展里),我们要做的第一件事就是检查应用包中是否存在一个初始设置文件: + +```swift +mutating func loadUsingSettingsFile() -> Bool { + guard let originalSettingsURL = Bundle.main.url(forResource: "\(Self.self)", withExtension: "plist") + else { return false } +} +``` + +**记住**:设置文件的名字应该和采纳了 `SettingsManageable` 的类或结构体名字相同。 + +下一步,我们将会检查缓存目录中是否已经存在属性列表文件。如果*不存在*,那么我们将*应用包中的文件拷贝到缓存目录中*: + +```swift +do { + if !FileManager.default.fileExists(atPath: settingsURL().path) { + try FileManager.default.copyItem(at: originalSettingsURL, to: settingsURL()) + } + +} catch { + print(error.localizedDescription) + return false +} +``` + +如果文件已经存在,条件判断则会为 false,那么拷贝的操作将不会执行!现在,我们要做的和之前一摸一样;我们会加载文件内容到 `Data` 对象中,然后我们会将其解码: + +```swift +do { + ... + + let fileContents = try Data(contentsOf: settingsURL()) + self = try PropertyListDecoder().decode(Self.self, from: fileContents) + return true + +} catch { ... } +``` + +无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常工作。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 + +## 删除设置文件 +对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: + +```swift +protocol SettingsManageable { + ... + + func delete() -> Bool +} +``` + +毫无保留地在协议扩展中提供方法的实现: + +```swift +func delete() -> Bool { + do { + try FileManager.default.removeItem(at: settingsURL()) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +如果移除文件像上面这样成功了,那么方法会返回 true。如果在移除过程中产生了任何错误,那么返回 false。 + +## 重置设置 +如果有办法可以在任意时间切换回最初的设置,那将会是个很有用的功能。这背后的含义其实是删除缓存目录中的属性列表文件,然后再把初始值写回去。 + +当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,当初始值被赋给了属性,事情就变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,又重新保存到文件中,这显然是错误的! + +为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得于第一份创建在缓存目录的文件。我们将会以它来重置设置为初始值,并且这样的话,我们也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 + +为了我刚刚提到的这个目的,我们还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以我们不会将它们定义在协议中。取而代之,我们会将其在协议扩展中实现的地方,并将它们标记为 `private`。第一个方法是将原始文件备份: + +```swift +private func backupSettingsFile() { + do { + try FileManager.default.copyItem(at: settingsURL(), to: settingsURL().appendingPathExtension("init")) + } catch { + print(error.localizedDescription) + } +} +``` + +拥有初始设置的文件将会以 “init” 作为文件扩展名。举个例子,原始设置文件叫 “AppSettings.plist”,那备份就会叫做 “AppSettings.plist.init”。 + +这个方法则是将初始设置拷贝到正常设置文件目录下: + +```swift +private func restoreSettingsFile() -> Bool { + do { + try FileManager.default.copyItem(at: settingsURL().appendingPathExtension("init"), to: settingsURL()) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +有了上面的两个方法,就让我们回到 `load()` 方法,在 `else` 条件下如果文件不存在,则会将属性列表文件第一次写入缓存目录。此刻这个地方看起来是这样: + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + ... + } else { + return update() + } +} +``` + +我们将会对其作出修改,我们会在 `update()` 方法之后调用 `backupSettingsFile()`,只要 `update()` 返回的是 true。这是修改之后的样子: + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + ... + } else { + if update() { + backupSettingsFile() + return true + } else { return false } + } +} +``` + +现在我们可以专注到重置方法的实现啦。在 `SettingsManageable` 协议中添加: + +```swift +protocol SettingsManageable { + ... + + mutating func reset() -> Bool +} +``` + +在协议扩展中实现它。我们先从删除当前缓存目录中的属性列表文件开始: + +```swift +mutating func reset() -> Bool { + if delete() { + + } + + return false +} +``` + +下一步我们要小心一点。我们想要让 `reset()` 方法与加载设置的方法独立开来。因为这个原因,我们会首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后我们会调用 `load()` 方法来加载设置。 + +这就是上面所描述「场景」的代码: + +```swift +mutating func reset() -> Bool { + if delete() { + if !loadUsingSettingsFile() { + if restoreSettingsFile() { + return load() + } + } else { + return true + } + } + return false +} +``` + +如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! + +# 将属性列表内容当作字典 +上面我们实现的所有的 `SettingsManageable` 协议中的方法所瞄准的目标,都是为所有采纳了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像我们遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以既然这篇文章给了机会可以聊一下属性列表,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如果将属性列表作为字典类型获取。 + +在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: + +```swift +protocol SettingsManageable { + ... + + func toDictionary() -> [String: Any?]? +} +``` + +这个方法会返回一个范性为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件无法被解码的过程中发生了什么糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 + +在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个我们之前已经做过好几次了。同时,我们还会把文件内容加载到 `Data` 对象中: + +```swift +func toDictionary() -> [String: Any?]? { + do { + if FileManager.default.fileExists(atPath: settingsURL().path) { + let fileContents = try Data(contentsOf: settingsURL()) + } + } catch { + print(error.localizedDescription) + } + + return nil +} +``` + +可以看到如果文件不存在我们将直接返回 nil。接下来是重点,`PropertyListSerialization` 类提供了一个叫作 `propertyList(from:options:format:)` 的方法,可以从 `Data` 对象中返回一个属性列表对象(在我们这里就是 `fileContents` 对象)。我们这么使用这个方法: + +```swift +let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) +``` + +作为一个会抛出异常错误的方法,使用 `try` 关键字是必要的。它的结果是返回一个 `Any` 类型的对象,所以我们可以通过简单的转换来得到一个字典: + +```swift +let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) as? [String: Any?] +``` + +`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就很小的。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。然而既然我们正在讨论属性列表,这就是个很好的机会提及此方法。 + +现在我们要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? + +嘿,我将这个问题留给你作为可选的练习。给一个小提示,如果是我的话,会去找一个叫 `data(fromPropertyList:format:options)` 的方法。 + +# SettingsManageable 协议实战 +是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! + +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合[***单例***](https://en.wikipedia.org/wiki/Singleton_pattern)的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实,然后在需要的时候,将其当作其他普通的类来初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是属性的默认值中设置的。 + +我们需要做的第一件事就是让 `AppSettings` 类采纳 `SettingsManageable` 协议。不要忘了也要采纳 `Codable` 协议: + +```swift +class AppSettings: Codable, SettingsManageable { + ... +} +``` + +因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过我们之前实现的 `load()` 方法来加载设置: + +```swift +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + + _ = AppSettings.shared.load() + + return true +} +``` + +如果应用第一次启动,那么当 `load()` 方法调用时,初始设置会被写入 “AppSettings.plist” 文件,保存在缓存目录中。与此同时,这些初始设置也会被被分到 “AppSettings.plist.init” 文件中,这样我们可以在需要的时候通过它来重置设置。如果 .plist 文件已经在应用启动的时候存在了,那么设置会被加载并且赋到 `AppSettings` 共享实例的属性上。 + +现在再到 `ViewController.swift` 文件,移动到 `tryAppSettings()` 方法中。我们将会修改一些设置: + +```swift +func tryAppSettings() { + AppSettings.shared.fontSize = 21.0 + AppSettings.shared.playSFX = false +} +``` + +让我们来更新一下设置文件,然后看看我们写入了什么: + +```swift +if AppSettings.shared.update() { + if let dictionary = AppSettings.shared.toDictionary() { + print(dictionary.compactMapValues { $0 }) + } +} +``` + +上面的代码片段除了 `update()` 之外我们还使用了 `toDictionary()` 方法,并且我们还将文件内容加载进了字典对象。然后我们将字典中除 nil 之外的值打印了出来。 + +同时,为了验证文件是否被成功创建,再添加一行代码,它会打印应用的真实的缓存目录,你可以通过 Finder 来查看: + +```swift +print(AppSettings.shared.settingsURL().path) +``` + +如果你运行应用,你就能在控制台中看到如下输出: + +![console](https://www.appcoda.com/wp-content/uploads/2019/08/t67_3_results_1.png) + +如果你通过 Finder 跳转到打印出来的路径,你还能看到有两个文件被创建: + +![files_created](https://www.appcoda.com/wp-content/uploads/2019/08/t67_4_files1.png) + +让我们试试重置设置。在上面的指令后面再添加下面的代码: + +```swift +if AppSettings.shared.reset() { + if let dictionary = AppSettings.shared.toDictionary() { + print(dictionary.compactMapValues { $0 }) + } +} +``` + +再次运行。这次你会先看到被修改的值,然后是原始值。这意味着我们重置到初始设置如预期的成功了! + +![reset_settings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_5_results_2.png) + +让我们切换到 *PlayerSettings.swift* 文件然后找到用来表示虚拟游戏玩家设置的 `PlayerSettings` 结构体。首先让 `PlayerSettings` 结构体采纳 `Codable` 和 `SettingsManageable` 协议: + +```swift +struct PlayerSettings: Codable, SettingsManageable { + ... +} +``` + +与 `AppSettings` 类所使用的属性相反,这里初始值并没有直接赋给属性。与之替代的是存在于应用包中的 *PlayerSettings.plist* 文件,你可以在 Project Navigator 中找到。当你打开文件时,注意在 .plist 文件中的键名和属性名相同。 + +回到 `ViewController.swift` 文件,找到 `tryPlayerSettings()` 方法。要做的第一件事是初始化一个 `PlayerSettings` 对象,然后加载设置,但这一次我们将会使用 `loadUsingSettingsFile()` 方法。同样的,我们也会像之前那样将结果打印出来: + +```swift +func tryPlayerSettings() { + var playerSettings = PlayerSettings() + if playerSettings.loadUsingSettingsFile() { + if let dictionary = playerSettings.toDictionary() { + print(dictionary.compactMapValues { $0 }) + } + } + +} +``` + +下面是我们运行应用之后打印在控制台的结果: +![console2](https://www.appcoda.com/wp-content/uploads/2019/08/t67_6_results_3.png) + +以及这是与 *AppSettings.plist* 同时存在的 *PlayerSettings.plist* 文件: +![PlayerSettings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_7_files2.png) + +让我们来修改一些属性,然后更新 .plist 文件,并且再次打印: + +```swift +playerSettings.isMale = false +playerSettings.gunType = 1 +playerSettings.powerLevels?[0] = 1 +playerSettings.powerLevels?[1] = 1 +playerSettings.powerLevels?[3] = 5 +playerSettings.powerLevels?[4] = 5 +_ = playerSettings.update() +if let dictionary = playerSettings.toDictionary() { + print("\n", dictionary.compactMapValues { $0 }) +} +``` + +注意在真正的应用中,我们应该检查下标是否越界! + +这里是从 .plist 文件中获取到的更新后的设置: +![updated_plist](https://www.appcoda.com/wp-content/uploads/2019/08/t67_8_results_4.png) + +最后,让我们尝试着重置设置,然后验证一下原始的设置文件是否会覆盖当前在缓存目录中的设置文件: + +```swift +_ = playerSettings.reset() +if let dictionary = playerSettings.toDictionary() { + print("\n", dictionary.compactMapValues { $0 }) +} +``` +![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) + +# 总结 +我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能性以及对于所有应用来说都很必要的自动化任务。同时,利用了我们这里提到的解决方案,增加了工作的效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! + +作为参考,你可以在 GitHub 上下载到[完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From 4100960501ef339d75feede4b0d78d830825e165 Mon Sep 17 00:00:00 2001 From: saitjr Date: Fri, 27 Dec 2019 15:28:25 +0800 Subject: [PATCH 02/20] =?UTF-8?q?Update=20=E6=88=90=E5=91=98=E4=BB=8B?= =?UTF-8?q?=E7=BB=8D.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\346\210\220\345\221\230\344\273\213\347\273\215.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\346\210\220\345\221\230\344\273\213\347\273\215.md" "b/\346\210\220\345\221\230\344\273\213\347\273\215.md" index 361d08a0..47dad812 100644 --- "a/\346\210\220\345\221\230\344\273\213\347\273\215.md" +++ "b/\346\210\220\345\221\230\344\273\213\347\273\215.md" @@ -26,7 +26,7 @@ #### (其他小伙伴贴的)各种标签: -资深豆瓣知乎玩家、QCon演讲嘉宾、人生导师、夸父莲、P9莲、偶像莲、ggtalk 常驻嘉宾、我是大家的老朋友莲叔(云南腔)、孤独优秀了四年、有一个什么都知道的朋友 +资深豆瓣知乎玩家、QCon演讲嘉宾、人生导师、夸父莲、P9莲、偶像莲、ggtalk 常驻嘉宾、我是大家的老朋友莲叔(云南腔)、孤独优秀了四年、有一个什么都知道的朋友、💉🔥🔥🔥、🐲 ## 梁杰 / numbbbbb From 0222946b881a0dc556d50e82314da154c1913ccc Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Mon, 30 Dec 2019 13:04:39 +0800 Subject: [PATCH 03/20] update swift-protocols-app-configuration --- ...91220_swift-protocols-app-configuration.md | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index fc68c577..e5e8f0d4 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -23,18 +23,18 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 -让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为一个 Swift 开发中的流行话题的原因。通过定义一组方法来描述一系列功能,并对他们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 +让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 -如果你是一个新手开发者,那么我强烈推荐你阅读[**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 +如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 -好了,那到底协议与这篇文章标题所说的应用的设置有什么关系吗?让我来解释一下,并且将其建立起联系。很久以来,我一直都被 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用他们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! +好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! # 线路图 -我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。我们将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,再到最后的一个指令,就会让它完全正常运转。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来我们将要创建的文件是 *属性列表*(.plist)。毕竟当我们考虑设置时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们让要加上对应用包中的默认初始设置的处理支持(那些我们可以在 Xcode 属性列表编辑器中编辑的设置)。 +我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 -在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里我们是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 -最后,这里有一个供你下载的[启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用来放在协议中改变。在你下载之后,请用 Xcode 打开然后继续往下。 +最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议进行改造。在你下载之后,请用 Xcode 打开然后往下阅读。 # 开始:创建协议 让我们打开启动项目,然后创建一个将用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 @@ -51,7 +51,7 @@ protocol SettingsManageable { } ``` -我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样我们可以通过采纳协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: +我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: ```swift extension SettingsManageable where Self: Codable { @@ -59,17 +59,17 @@ extension SettingsManageable where Self: Codable { } ``` -注意我们在这里设置的条件。通过扩展尾部的 `where Self: Codable` 条件,我们要求了任何自定义类型在采纳 `SettingsManageable` 协议的同时,也要采纳 [`Codable`](https://developer.apple.com/documentation/swift/codable) 协议。 +注意在这里设置的条件。通过扩展尾部的 `where Self: Codable` 条件,我们要求了任何自定义类型在遵循 `SettingsManageable` 协议的同时,也要遵循 [`Codable`](https://developer.apple.com/documentation/swift/codable) 协议。 -为什么我们需要 `Codable`?因为我们会对采纳了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 +为什么我们需要 `Codable`?因为我们会对遵循了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 -以防止有的类型并未采纳 `Codable` 但采纳了 `SettingsManageable`,这样就别指望我们接下来能够为其提供正确的功能了。 +以免有的类型并未遵循 `Codable` 但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 # 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何采纳了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! ## 获取文件 URL -任何采纳了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! +任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! 向协议中添加下面方法的定义: @@ -90,12 +90,12 @@ extension SettingsManageable where Self: Codable { } ``` -第一行我们从缓存目录获取到了 URL。第二行我们将*采纳了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 +第一行从缓存目录获取到了 URL。第二行将*遵循了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 -注意 `\(Self.self)` 动态地提供了采纳 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类采纳了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体采纳了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 +注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 ## 存储(更新)一个 SettingsManageable 实例 -毋庸置疑我们上面实现的方法是非常实用的,但是它也只是一个为了我们方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将采纳协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: +毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 2. 写入文件 @@ -109,7 +109,7 @@ protocol SettingsManageable { } ``` -我们将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 +将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 对象转化成 JSON。有很大概率你已经用过了它!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? @@ -127,7 +127,7 @@ func update() -> Bool { } ``` -如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先我们创建了一个 `PropertyListEncoder` 的类,然后我们调用了 `encode(_:)` 方法来对 `self`(采纳了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! +如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先创建了一个 `PropertyListEncoder` 的类,然后调用了 `encode(_:)` 方法来对 `self`(遵循了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! 如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里我们并未对异常做任何额外操作,我们只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 @@ -158,7 +158,7 @@ func update() -> Bool { } ``` -在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望我们的方法内部使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: +在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望方法内部所使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: ```swift func update() throws @@ -173,12 +173,12 @@ func update() throws { } ``` -可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了方便你,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 +可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了让你方便,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 -> 注意:想要了解更多关于错误处理的内容?看看[这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)吧! +> 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html) 吧! ## 通过文件加载设置 -现在让我们过渡到通过文件加载数据,我们刚刚提到的保存数据的对应部分。我们要跟随的步骤很简单,所有要做的就以下几点: +现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。我们要跟随的步骤很简单,只需要跟着下面这几步: 1. 检查文件是否存在。 2. 将文件内容加载为 `Data` 对象。 @@ -193,7 +193,7 @@ protocol SettingsManageable { } ``` -这个方法被标记为*可变*,因为它会修改采纳了 `SettingsManageable` 协议的类型的实例(简单来说,它会修改 `self`)。 +这个方法被标记为*可变*,因为它会修改遵循了 `SettingsManageable` 协议的类型的实例(简单来说,它会修改 `self`)。 这个方法的实现其实很简单并且和我们之前做的很相似。只是额外增加了对文件是否存在的检查。在协议扩展的方法中添加如下实现: @@ -212,9 +212,9 @@ mutating func load() -> Bool { } ``` -我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次我们需要使用 `path` 属性来获取路径字符串。当我们确定文件存在之后,我们就从文件中加载内容到 `fileContents` 实例中,然后我们就拿到加载的数据,通过 `PropertyListDecoder` 类解码后,对 `self`(采纳了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,我们就返回 true,如果出现了错误则会抛出异常,然后我们会将错误打印在控制台并且返回 false。 +我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次需要使用 `path` 属性来获取路径字符串。当确定文件存在之后,就从文件中加载内容到 `fileContents` 实例中,拿到加载的数据后,通过 `PropertyListDecoder` 类解码后,对 `self`(遵循了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,返回 true,如果出现了错误则会抛出异常,然后将错误打印在控制台并且返回 false。 -现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况我们还没有返回值。如果没有文件可以加载设置,那么我们就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! +现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况还没有返回值。如果没有文件可以加载设置,那么就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! ```swift mutating func load() -> Bool { @@ -226,16 +226,16 @@ mutating func load() -> Bool { } ``` -现在方法在所有的情况下都有返回值了。如果我们第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 +现在方法在所有的情况下都有返回值了。如果是第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 ## 通过应用包中的 Plist 文件初始化设置 -既然我们一直在聊设置,那么很自然就会想到他们的的默认值。在编程层面,采纳了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者唯一适合的方式。通过 Xcode 的属性列表编辑器来编辑放在应用包中的属性列表文件,以初始化属性列表文件或许是更好也跟简单的方式。我们将要在这个部分覆盖这种情况,然后还有**一条很重要的规则**需要继续: +既然我们一直在聊设置,那么很自然就会想到它们的的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: *属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* 否则的话,解码会失败。 -让我们通过在 `SettingsManageable` 协议中定义一个新的方法开始吧: +让我们从 `SettingsManageable` 协议中定义一个新的方法开始吧: ```swift protocol SettingsManageable { @@ -245,7 +245,7 @@ protocol SettingsManageable { } ``` -在这个方法的实现中(协议扩展里),我们要做的第一件事就是检查应用包中是否存在一个初始设置文件: +在这个方法的实现中(协议扩展里),要做的第一件事就是检查应用包中是否存在一个初始设置文件: ```swift mutating func loadUsingSettingsFile() -> Bool { @@ -254,9 +254,9 @@ mutating func loadUsingSettingsFile() -> Bool { } ``` -**记住**:设置文件的名字应该和采纳了 `SettingsManageable` 的类或结构体名字相同。 +**记住**:设置文件的名字应该和遵循了 `SettingsManageable` 的类或结构体名字相同。 -下一步,我们将会检查缓存目录中是否已经存在属性列表文件。如果*不存在*,那么我们将*应用包中的文件拷贝到缓存目录中*: +下一步,我们将会检查缓存目录中是否已经存在属性列表文件。如果*不存在*,那么就将*应用包中的文件拷贝到缓存目录中*: ```swift do { @@ -270,7 +270,7 @@ do { } ``` -如果文件已经存在,条件判断则会为 false,那么拷贝的操作将不会执行!现在,我们要做的和之前一摸一样;我们会加载文件内容到 `Data` 对象中,然后我们会将其解码: +如果文件已经存在,条件判断则会为 false,那么拷贝的操作将不会执行!现在,要做的和之前一模一样;我们会加载文件内容到 `Data` 对象中,然后将其解码: ```swift do { @@ -296,7 +296,7 @@ protocol SettingsManageable { } ``` -毫无保留地在协议扩展中提供方法的实现: +协议扩展里的方法实现不是什么秘密: ```swift func delete() -> Bool { @@ -317,9 +317,9 @@ func delete() -> Bool { 当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,当初始值被赋给了属性,事情就变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,又重新保存到文件中,这显然是错误的! -为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得于第一份创建在缓存目录的文件。我们将会以它来重置设置为初始值,并且这样的话,我们也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 +为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得是第一次在缓存目录创建的文件。我们将会以它来重置设置为初始值,并且这样的话,也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 -为了我刚刚提到的这个目的,我们还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以我们不会将它们定义在协议中。取而代之,我们会将其在协议扩展中实现的地方,并将它们标记为 `private`。第一个方法是将原始文件备份: +为了达到这个目的,还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以不会将它们定义在协议中。取而代之的是在协议扩展中方法实现的地方将它们标记为 `private`。第一个方法是将原始文件备份: ```swift private func backupSettingsFile() { @@ -347,7 +347,7 @@ private func restoreSettingsFile() -> Bool { } ``` -有了上面的两个方法,就让我们回到 `load()` 方法,在 `else` 条件下如果文件不存在,则会将属性列表文件第一次写入缓存目录。此刻这个地方看起来是这样: +有了上面的两个方法,让我们回到 `load()` 方法,在 `else` 条件下如果文件不存在,则会将属性列表文件第一次写入缓存目录。此刻这个地方看起来是这样: ```swift mutating func load() -> Bool { @@ -396,7 +396,7 @@ mutating func reset() -> Bool { } ``` -下一步我们要小心一点。我们想要让 `reset()` 方法与加载设置的方法独立开来。因为这个原因,我们会首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后我们会调用 `load()` 方法来加载设置。 +下一步要小心一点。为了让 `reset()` 方法与加载设置的方法独立开来。因此,我们要首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后调用 `load()` 方法来加载设置。 这就是上面所描述「场景」的代码: @@ -418,7 +418,7 @@ mutating func reset() -> Bool { 如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! # 将属性列表内容当作字典 -上面我们实现的所有的 `SettingsManageable` 协议中的方法所瞄准的目标,都是为所有采纳了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像我们遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以既然这篇文章给了机会可以聊一下属性列表,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如果将属性列表作为字典类型获取。 +所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: @@ -430,9 +430,9 @@ protocol SettingsManageable { } ``` -这个方法会返回一个范性为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件无法被解码的过程中发生了什么糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 +这个方法会返回一个范型为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件在被解码的过程中,发生了如无法解码这样糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 -在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个我们之前已经做过好几次了。同时,我们还会把文件内容加载到 `Data` 对象中: +在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个之前已经做过好几次了。同时还要把文件内容加载到 `Data` 对象中: ```swift func toDictionary() -> [String: Any?]? { @@ -448,30 +448,30 @@ func toDictionary() -> [String: Any?]? { } ``` -可以看到如果文件不存在我们将直接返回 nil。接下来是重点,`PropertyListSerialization` 类提供了一个叫作 `propertyList(from:options:format:)` 的方法,可以从 `Data` 对象中返回一个属性列表对象(在我们这里就是 `fileContents` 对象)。我们这么使用这个方法: +可以看到如果文件不存在将直接返回 nil。接下来是重点,`PropertyListSerialization` 类提供了一个叫作 `propertyList(from:options:format:)` 的方法,可以从 `Data` 对象中返回一个属性列表对象(在我们这里就是 `fileContents` 对象)。方法这么使用: ```swift let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) ``` -作为一个会抛出异常错误的方法,使用 `try` 关键字是必要的。它的结果是返回一个 `Any` 类型的对象,所以我们可以通过简单的转换来得到一个字典: +作为一个会抛出异常错误的方法,使用 `try` 关键字是必要的。它的结果是返回一个 `Any` 类型的对象,所以可以通过简单的转换来得到一个字典: ```swift let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) as? [String: Any?] ``` -`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就很小的。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。然而既然我们正在讨论属性列表,这就是个很好的机会提及此方法。 +`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。自从有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就变小了。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。既然我们正在讨论属性列表,就可以顺便提及此方法。 -现在我们要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? +现在要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? 嘿,我将这个问题留给你作为可选的练习。给一个小提示,如果是我的话,会去找一个叫 `data(fromPropertyList:format:options)` 的方法。 # SettingsManageable 协议实战 是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! -首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合[***单例***](https://en.wikipedia.org/wiki/Singleton_pattern)的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实,然后在需要的时候,将其当作其他普通的类来初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是属性的默认值中设置的。 +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 -我们需要做的第一件事就是让 `AppSettings` 类采纳 `SettingsManageable` 协议。不要忘了也要采纳 `Codable` 协议: +我们需要做的第一件事就是让 `AppSettings` 类遵循 `SettingsManageable` 协议。不要忘了也要遵循 `Codable` 协议: ```swift class AppSettings: Codable, SettingsManageable { @@ -479,7 +479,7 @@ class AppSettings: Codable, SettingsManageable { } ``` -因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过我们之前实现的 `load()` 方法来加载设置: +因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过之前实现的 `load()` 方法来加载设置: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -491,7 +491,7 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau } ``` -如果应用第一次启动,那么当 `load()` 方法调用时,初始设置会被写入 “AppSettings.plist” 文件,保存在缓存目录中。与此同时,这些初始设置也会被被分到 “AppSettings.plist.init” 文件中,这样我们可以在需要的时候通过它来重置设置。如果 .plist 文件已经在应用启动的时候存在了,那么设置会被加载并且赋到 `AppSettings` 共享实例的属性上。 +如果应用第一次启动,那么当 `load()` 方法调用时,初始设置会被写入 “AppSettings.plist” 文件,保存在缓存目录中。与此同时,这些初始设置也会被备份到 “AppSettings.plist.init” 文件中,这样可以在需要的时候通过它来重置设置。如果 .plist 文件已经在应用启动的时候存在了,那么设置会被加载并且赋到 `AppSettings` 共享实例的属性上。 现在再到 `ViewController.swift` 文件,移动到 `tryAppSettings()` 方法中。我们将会修改一些设置: @@ -502,7 +502,7 @@ func tryAppSettings() { } ``` -让我们来更新一下设置文件,然后看看我们写入了什么: +让我们来更新一下设置文件,然后看看写入了什么: ```swift if AppSettings.shared.update() { @@ -512,7 +512,7 @@ if AppSettings.shared.update() { } ``` -上面的代码片段除了 `update()` 之外我们还使用了 `toDictionary()` 方法,并且我们还将文件内容加载进了字典对象。然后我们将字典中除 nil 之外的值打印了出来。 +上面的代码片段除了 `update()` 之外还使用了 `toDictionary()` 方法,并且还将文件内容加载进了字典对象。然后我们将字典中除 nil 之外的值打印了出来。 同时,为了验证文件是否被成功创建,再添加一行代码,它会打印应用的真实的缓存目录,你可以通过 Finder 来查看: @@ -538,11 +538,11 @@ if AppSettings.shared.reset() { } ``` -再次运行。这次你会先看到被修改的值,然后是原始值。这意味着我们重置到初始设置如预期的成功了! +再次运行。这次你会先看到被修改的值,然后是原始值。这意味着重置到初始设置如预期的成功了! ![reset_settings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_5_results_2.png) -让我们切换到 *PlayerSettings.swift* 文件然后找到用来表示虚拟游戏玩家设置的 `PlayerSettings` 结构体。首先让 `PlayerSettings` 结构体采纳 `Codable` 和 `SettingsManageable` 协议: +切换到 *PlayerSettings.swift* 文件然后找到用来表示虚拟游戏玩家设置的 `PlayerSettings` 结构体。首先让 `PlayerSettings` 结构体遵循 `Codable` 和 `SettingsManageable` 协议: ```swift struct PlayerSettings: Codable, SettingsManageable { @@ -552,7 +552,7 @@ struct PlayerSettings: Codable, SettingsManageable { 与 `AppSettings` 类所使用的属性相反,这里初始值并没有直接赋给属性。与之替代的是存在于应用包中的 *PlayerSettings.plist* 文件,你可以在 Project Navigator 中找到。当你打开文件时,注意在 .plist 文件中的键名和属性名相同。 -回到 `ViewController.swift` 文件,找到 `tryPlayerSettings()` 方法。要做的第一件事是初始化一个 `PlayerSettings` 对象,然后加载设置,但这一次我们将会使用 `loadUsingSettingsFile()` 方法。同样的,我们也会像之前那样将结果打印出来: +回到 `ViewController.swift` 文件,找到 `tryPlayerSettings()` 方法。要做的第一件事是初始化一个 `PlayerSettings` 对象,然后加载设置,但这一次将会使用 `loadUsingSettingsFile()` 方法。同样的,我们也会像之前那样将结果打印出来: ```swift func tryPlayerSettings() { @@ -566,13 +566,13 @@ func tryPlayerSettings() { } ``` -下面是我们运行应用之后打印在控制台的结果: +下面是运行应用之后打印在控制台的结果: ![console2](https://www.appcoda.com/wp-content/uploads/2019/08/t67_6_results_3.png) 以及这是与 *AppSettings.plist* 同时存在的 *PlayerSettings.plist* 文件: ![PlayerSettings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_7_files2.png) -让我们来修改一些属性,然后更新 .plist 文件,并且再次打印: +再来修改一些属性,然后更新 .plist 文件,并且再次打印: ```swift playerSettings.isMale = false @@ -592,7 +592,7 @@ if let dictionary = playerSettings.toDictionary() { 这里是从 .plist 文件中获取到的更新后的设置: ![updated_plist](https://www.appcoda.com/wp-content/uploads/2019/08/t67_8_results_4.png) -最后,让我们尝试着重置设置,然后验证一下原始的设置文件是否会覆盖当前在缓存目录中的设置文件: +最后,来尝试着重置设置,然后验证一下原始的设置文件是否会覆盖当前在缓存目录中的设置文件: ```swift _ = playerSettings.reset() @@ -603,6 +603,6 @@ if let dictionary = playerSettings.toDictionary() { ![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) # 总结 -我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能性以及对于所有应用来说都很必要的自动化任务。同时,利用了我们这里提到的解决方案,增加了工作的效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! +我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能以及提供所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! -作为参考,你可以在 GitHub 上下载到[完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file +作为参考,你可以在 GitHub 上下载到 [完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From 6ba521a0ea0e5bf633165f74aacaf37ba0b5b9b2 Mon Sep 17 00:00:00 2001 From: Nemocdz Date: Mon, 6 Jan 2020 11:08:26 +0800 Subject: [PATCH 04/20] update source --- ...20\347\253\231\350\256\260\345\275\225.md" | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git "a/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" "b/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" index 34522408..1cf893d4 100644 --- "a/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" +++ "b/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" @@ -4,23 +4,23 @@ | 网站 | 最后检查时间 | 最后收录文章 | 备注 | | ------------------------------------------------------------ | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 作者:Mattt
网址:[NSHipster](https://nshipster.com/) | 2019.11.7 | 原文:[Device Identifiers and Fingerprinting on iOS](https://nshipster.com/device-identifiers/)
Issue: [#275](https://github.com/SwiftGGTeam/translation/issues/275) | 参考 [NSHipster 翻译说明](https://github.com/SwiftGGTeam/translation/blob/master/NSHipster%E7%BF%BB%E8%AF%91%E8%AF%B4%E6%98%8E.md) | -| 作者:Helge Hess
网址:[Always Right Institute](http://www.alwaysrightinstitute.com/) | 2019.11.7 | 原文:[Dynamic Environments ¶ SwiftUI RulesI](http://www.alwaysrightinstitute.com/swiftuirules//)
Issue: [#263](https://github.com/SwiftGGTeam/translation/issues/263) | | -| 作者:Erica Sadun
网址:[Erica Sadun](http://ericasadun.com/) | 2019.11.7 | 原文:[The Beauty of Swift 5 String Interpolation](https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/)
Issue: [#175](https://github.com/SwiftGGTeam/translation/issues/175) | 文章一般比较短,话痨,口语化,不太好翻译,挑有价值的即可 | -| 作者:Umberto Raimondi
网址:[uraimo](https://www.uraimo.com/) | 2019.11.7 | 原文:[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)
Issue:[#47](https://github.com/SwiftGGTeam/translation/issues/47) | 2018,2019 都只更新了树莓派相关的内容 | -| 作者:Soroush Khanlou
网址:[KHANLOU](http://khanlou.com/) | 2019.11.7 | 原文:[Linear Probing](http://khanlou.com/2019/08/linear-probing/)
Issue:[#255](https://github.com/SwiftGGTeam/translation/issues/255) | | -| 作者:Russ Bishop
网址:[Russ Bishop](http://www.russbishop.net/) | 2019.11.7 | 原文:[Cross-process Rendering:Surfaces save us](http://www.russbishop.net/cross-process-rendering)
Issue:[#276](https://github.com/SwiftGGTeam/translation/issues/276) | 作者喜欢写副标题,将副标题以冒号形式接在主标题后面 | -| 作者:待定
网址:[AppCoda](https://www.appcoda.com/category/tutorials/ios/) | 2019.11.7 | 原文:[A Complete Guide to In-App Purchases for iOS Development](https://www.appcoda.com/in-app-purchases-guide/)
Issue:[#277](https://github.com/SwiftGGTeam/translation/issues/277) | 注意每篇文章作者不一样,Swift 分类很久不更新,主要看 iOS 分类即可,另有 [台湾站](https://www.appcoda.com.tw),文章不全,翻译可做参考 | -| 作者:Arthur Knopper
网址:[IOSCREATOR](http://www.ioscreator.com/) | 2019.11.7 | 原文:[Add Event to Calendar iOS Tutorial](https://www.appcoda.com/arkit-face-tracking/)
Issue:[#176](https://github.com/SwiftGGTeam/translation/issues/176) | 文章很多,都是很初级的内容,挑新的有价值的即可 | -| 作者:Nick Hanan
网址:[Coding Explorer Blog](http://www.codingexplorer.com/) | 2019.11.7 | 原文:[Error Handling in Swift](https://www.codingexplorer.com/error-handling-swift/)
Issue:[#53](https://github.com/SwiftGGTeam/translation/issues/53) | 作者有时候会更新老文章,比如我们收录的这篇,2019 年又更新过一次,注意别重复添加 | -| 作者:Thomas Hanning
网址:[Thomas Hanning](http://www.thomashanning.com/) | 2019.11.7 | 原文:[Sets in Swift](http://www.thomashanning.com/sets-in-swift/)
Issue:[#156](https://github.com/SwiftGGTeam/translation/issues/156) | | -| 作者:Ole Begemann
网址:[Ole Begemann](http://oleb.net/blog/) | 2019.11.7 | 原文:[Chris Lattner on the origins of Swift](https://oleb.net/2019/chris-lattner-swift-origins/)
Issue:[#212](https://github.com/SwiftGGTeam/translation/issues/212) | | -| 作者:Olivier Halligon
网址:[Crunchy Development](http://alisoftware.github.io/) | 2019.11.7 | 原文:[StringInterpolation in Swift 5 — AttributedStrings](https://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/)
Issue:[#207](https://github.com/SwiftGGTeam/translation/issues/207) | 2019 还没更新过 | -| 作者:Benedikt Terhechte
网址:[APPVENTURE](http://appventure.me/) | 2019.11.7 | 原文:[The usefulness of typealiases in swift](http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.htmll)
Issue:[#240](https://github.com/SwiftGGTeam/translation/issues/240) | | -| 作者:Mike Ash
网址:[NSBlog](https://www.mikeash.com/pyblog/) | 2019.11.7 | 原文:[objc_msgSend's New Prototype](https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html)
Issue: [#278](https://github.com/SwiftGGTeam/translation/issues/278) | 高质量博客,更新频率不高 | -| 作者:Dominik Hauser
网址:[Swift and Painless](http://swiftandpainless.com/) | 2019.11.7 | 原文:[#selector() and the responder chain](http://swiftandpainless.com/selector-and-the-responder-chain/)
Issue: [#49](https://github.com/SwiftGGTeam/translation/issues/49) | 2018、2019 都没更新过 | -| 作者:Weston Hanners
网址:[FRESHLY SQUOZEN WOOJI JUICE](http://www.wooji-juice.com/blog/) | 2019.11.7 | 原文:[Stupid Swift Tricks #6: To Animate or Not to Animate](http://www.wooji-juice.com/blog/stupid-swift-tricks-6-animations.html)
Issue:[#163](https://github.com/SwiftGGTeam/translation/issues/163) | 2019 还没更新过 | -| 作者:Tomasz Szulc
网址:[Tomasz Szulc](http://szulctomasz.com/programming-blog/) | 2019.11.7 | 原文:[Add fireworks and sparks to a UIView](http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/)
Issue:[#168](https://github.com/SwiftGGTeam/translation/issues/168) | 2019 还没更新过 | -| 作者:Jameson Quave
网址:[JamesonQuave.com](http://jamesonquave.com/) | 2019.11.7 | 原文:[Core ML for iOS Apps in iOS 11](http://jamesonquave.com/blog/core-ml-for-ios-apps-in-ios-11/)
Issue:[#39](https://github.com/SwiftGGTeam/translation/issues/39) | 2018、2019 都没更新过 | -| 作者:Jacob Bandes-Storch
网址:[Jacob Bandes-Storch](http://bandes-stor.ch/archive/) | 2019.11.7 | 原文:[Help Yourself to Some Swift](https://bandes-stor.ch/blog/2015/11/28/help-yourself-to-some-swift/)
Issue:无 | 作者现在好像不写 Swift 相关文章了,很久没更新 | -| 作者:Natasha
网址:[Natasha the Robot](http://natashatherobot.com/) | 2019.11.7 | 原文:[Architecting for Features](https://www.natashatherobot.com/architecting-for-features/[#](https://github.com/SwiftGGTeam/translation/issues/))
Issue:无 | 2019 还没更新过 | +| 作者:Mattt
网址:[NSHipster](https://nshipster.com/) | 2020.1.6 | 原文:[Objective-C Direct Methods](https://nshipster.com/direct/)
Issue: [#290](https://github.com/SwiftGGTeam/translation/issues/290) | 参考 [NSHipster 翻译说明](https://github.com/SwiftGGTeam/translation/blob/master/NSHipster%E7%BF%BB%E8%AF%91%E8%AF%B4%E6%98%8E.md) | +| 作者:Helge Hess
网址:[Always Right Institute](http://www.alwaysrightinstitute.com/) | 2020.1.6 | 原文:[Dynamic Environments ¶ SwiftUI RulesI](http://www.alwaysrightinstitute.com/swiftuirules//)
Issue: [#263](https://github.com/SwiftGGTeam/translation/issues/263) | | +| 作者:Erica Sadun
网址:[Erica Sadun](http://ericasadun.com/) | 2020.1.6 | 原文:[The Beauty of Swift 5 String Interpolation](https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/)
Issue: [#175](https://github.com/SwiftGGTeam/translation/issues/175) | 文章一般比较短,话痨,口语化,不太好翻译,挑有价值的即可 | +| 作者:Umberto Raimondi
网址:[uraimo](https://www.uraimo.com/) | 2020.1.6 | 原文:[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)
Issue:[#47](https://github.com/SwiftGGTeam/translation/issues/47) | 2018、2019、2020 都只更新了树莓派相关的内容 | +| 作者:Soroush Khanlou
网址:[KHANLOU](http://khanlou.com/) | 2019.1.6 | 原文:[Regexes vs Combinatorial Parsing](http://khanlou.com/2019/12/regex-vs-combinatorial-parsing/)
Issue:[#291](https://github.com/SwiftGGTeam/translation/issues/291) | | +| 作者:Russ Bishop
网址:[Russ Bishop](http://www.russbishop.net/) | 2020.1.6 | 原文:[Cross-process Rendering:Surfaces save us](http://www.russbishop.net/cross-process-rendering)
Issue:[#276](https://github.com/SwiftGGTeam/translation/issues/276) | 作者喜欢写副标题,将副标题以冒号形式接在主标题后面 | +| 作者:待定
网址:[AppCoda](https://www.appcoda.com/category/tutorials/ios/) | 2020.1.6 | 原文:[A Complete Guide to In-App Purchases for iOS Development](https://www.appcoda.com/in-app-purchases-guide/)
Issue:[#277](https://github.com/SwiftGGTeam/translation/issues/277) | 注意每篇文章作者不一样,Swift 分类很久不更新,主要看 iOS 分类即可,另有 [台湾站](https://www.appcoda.com.tw),文章不全,翻译可做参考 | +| 作者:Arthur Knopper
网址:[IOSCREATOR](http://www.ioscreator.com/) | 2020.1.6 | 原文:[Add Event to Calendar iOS Tutorial](https://www.appcoda.com/arkit-face-tracking/)
Issue:[#176](https://github.com/SwiftGGTeam/translation/issues/176) | 文章很多,都是很初级的内容,挑新的有价值的即可 | +| 作者:Nick Hanan
网址:[Coding Explorer Blog](http://www.codingexplorer.com/) | 2020.1.6 | 原文:[Error Handling in Swift](https://www.codingexplorer.com/error-handling-swift/)
Issue:[#53](https://github.com/SwiftGGTeam/translation/issues/53) | 作者有时候会更新老文章,比如我们收录的这篇,2019 年又更新过一次,注意别重复添加 | +| 作者:Thomas Hanning
网址:[Thomas Hanning](http://www.thomashanning.com/) | 2020.1.6 | 原文:[Sets in Swift](http://www.thomashanning.com/sets-in-swift/)
Issue:[#156](https://github.com/SwiftGGTeam/translation/issues/156) | | +| 作者:Ole Begemann
网址:[Ole Begemann](http://oleb.net/blog/) | 2020.1.6 | 原文:[Chris Lattner on the origins of Swift](https://oleb.net/2019/chris-lattner-swift-origins/)
Issue:[#212](https://github.com/SwiftGGTeam/translation/issues/212) | | +| 作者:Olivier Halligon
网址:[Crunchy Development](http://alisoftware.github.io/) | 2020.1.6 | 原文:[StringInterpolation in Swift 5 — AttributedStrings](https://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/)
Issue:[#207](https://github.com/SwiftGGTeam/translation/issues/207) | 2019、2020 还没更新过 | +| 作者:Benedikt Terhechte
网址:[APPVENTURE](http://appventure.me/) | 2020.1.6 | 原文:[The usefulness of typealiases in swift](http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.htmll)
Issue:[#240](https://github.com/SwiftGGTeam/translation/issues/240) | | +| 作者:Mike Ash
网址:[NSBlog](https://www.mikeash.com/pyblog/) | 2020.1.6 | 原文:[objc_msgSend's New Prototype](https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html)
Issue: [#278](https://github.com/SwiftGGTeam/translation/issues/278) | 高质量博客,更新频率不高 | +| 作者:Dominik Hauser
网址:[Swift and Painless](http://swiftandpainless.com/) | 2020.1.6 | 原文:[#selector() and the responder chain](http://swiftandpainless.com/selector-and-the-responder-chain/)
Issue: [#49](https://github.com/SwiftGGTeam/translation/issues/49) | 2018、2019、2020 都没更新过 | +| 作者:Weston Hanners
网址:[FRESHLY SQUOZEN WOOJI JUICE](http://www.wooji-juice.com/blog/) | 2020.1.6 | 原文:[Stupid Swift Tricks #6: To Animate or Not to Animate](http://www.wooji-juice.com/blog/stupid-swift-tricks-6-animations.html)
Issue:[#163](https://github.com/SwiftGGTeam/translation/issues/163) | 2019、2020 还没更新过 | +| 作者:Tomasz Szulc
网址:[Tomasz Szulc](http://szulctomasz.com/programming-blog/) | 2020.1.6 | 原文:[Add fireworks and sparks to a UIView](http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/)
Issue:[#168](https://github.com/SwiftGGTeam/translation/issues/168) | 2019、2020 还没更新过 | +| 作者:Jameson Quave
网址:[JamesonQuave.com](http://jamesonquave.com/) | 2020.1.6 | 原文:[Core ML for iOS Apps in iOS 11](http://jamesonquave.com/blog/core-ml-for-ios-apps-in-ios-11/)
Issue:[#39](https://github.com/SwiftGGTeam/translation/issues/39) | 2018、2019 、2020 都没更新过 | +| 作者:Jacob Bandes-Storch
网址:[Jacob Bandes-Storch](http://bandes-stor.ch/archive/) | 2020.1.6 | 原文:[Help Yourself to Some Swift](https://bandes-stor.ch/blog/2015/11/28/help-yourself-to-some-swift/)
Issue:无 | 作者现在好像不写 Swift 相关文章了,很久没更新 | +| 作者:Natasha
网址:[Natasha the Robot](http://natashatherobot.com/) | 2020.1.6 | 原文:[Architecting for Features](https://www.natashatherobot.com/architecting-for-features/[#](https://github.com/SwiftGGTeam/translation/issues/))
Issue:无 | 2019、2020 还没更新过 | From 9b842e3dff8c440067732e6b3e99dd5137adb00d Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Wed, 8 Jan 2020 14:59:33 +0800 Subject: [PATCH 05/20] update swift-protocols-app-configuration(v2) --- .../20191220_swift-protocols-app-configuration.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index e5e8f0d4..b671a25f 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -16,23 +16,23 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 定稿= -大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我不认为有任何一个程序员不知道协议。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: +大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我认为不会有不知道协议的程序员。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: > 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是满足了该协议。 用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 -让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 +协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! # 线路图 -我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 -在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议进行改造。在你下载之后,请用 Xcode 打开然后往下阅读。 @@ -51,7 +51,7 @@ protocol SettingsManageable { } ``` -我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: +我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: ```swift extension SettingsManageable where Self: Codable { @@ -319,7 +319,7 @@ func delete() -> Bool { 为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得是第一次在缓存目录创建的文件。我们将会以它来重置设置为初始值,并且这样的话,也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 -为了达到这个目的,还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以不会将它们定义在协议中。取而代之的是在协议扩展中方法实现的地方将它们标记为 `private`。第一个方法是将原始文件备份: +为了达到这个目的,还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望它们可以被显式调用,所以不会将它们定义在协议中。取而代之的是在协议扩展中方法实现的地方将它们标记为 `private`。第一个方法是将原始文件备份: ```swift private func backupSettingsFile() { @@ -418,7 +418,7 @@ mutating func reset() -> Bool { 如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! # 将属性列表内容当作字典 -所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 +所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: From 414f11a33363bd85b8f1173eb6091c515a033f6f Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Thu, 30 Jan 2020 14:26:19 +0800 Subject: [PATCH 06/20] update swift-protocols-app-configuration(v3) --- ...91220_swift-protocols-app-configuration.md | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index b671a25f..8016ab32 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -16,12 +16,12 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 定稿= -大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我认为不会有不知道协议的程序员。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: +大家好,欢迎阅读这篇新教程!协议是广大程序员在使用 Swift 时最常接触和使用的概念之一,我认为不会有不知道协议的程序员。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: -> 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是满足了该协议。 +> 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是遵循了该协议。 -用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 +用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议遵循类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 @@ -29,17 +29,21 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! +通过协议定义一系列负责文件处理的方法,以及通过协议扩展提供这些方法的默认实现。这样一来每一个遵循了协议的类或结构体,都能够获得相同的文件相关的功能。不仅如此,支持了各种设置的类或结构体还能以这种方式同时存在于应用中,这些数据也能被分别处理。换句话说,这种协议就像是某种即插即用机制,可以使任何遵循它的自定义类型具备保存和读取数据的能力。 + +这是我长期以来都在使用的解决方案,现在是时候将它拿出来在这讨论了。这篇文章所讨论的内容很显然是针对 Swift 开发新人的,可以让他们了解协议,以及协议的实际用法。毫无疑问高级开发者们早已有了类似的解决方法,但无论你处于什么阶段,都推荐继续阅读下去。 + # 线路图 -我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +我们今天的目标是创建一个可以被任何类型遵循,支持简单的保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 -在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里处理的是属性列表数据,但也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型,比如 JSON 或是纯文本。 -最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议进行改造。在你下载之后,请用 Xcode 打开然后往下阅读。 +最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议对其改造。在你下载之后,请用 Xcode 打开然后往下阅读。 # 开始:创建协议 -让我们打开启动项目,然后创建一个将用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 +让我们打开启动项目,然后创建一个用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 -下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 Create 按钮,来让 Xcode 真正地创建新文件。 +下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 *Create* 按钮,来让 Xcode 真正地创建新文件。 > 注意: 这篇文章中的代码是在 Xcode 10.3 中创建的,这是在撰写这篇文章时最新且最稳定的 Xcode 版本。 @@ -51,7 +55,7 @@ protocol SettingsManageable { } ``` -我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: +我之前简单地解释过,我们将会在 `SettingsManageable` 协议中定义方法,然后提供默认的行为实现,这样可以通过遵循协议来得到我们想要的功能。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: ```swift extension SettingsManageable where Self: Codable { @@ -63,10 +67,10 @@ extension SettingsManageable where Self: Codable { 为什么我们需要 `Codable`?因为我们会对遵循了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 -以免有的类型并未遵循 `Codable` 但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 +这样可以避免有的类型没有遵循 `Codable`,但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 # 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! ## 获取文件 URL 任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! @@ -95,7 +99,7 @@ extension SettingsManageable where Self: Codable { 注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 ## 存储(更新)一个 SettingsManageable 实例 -毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: +毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 2. 写入文件 @@ -109,9 +113,9 @@ protocol SettingsManageable { } ``` -将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 +将其命名为 `update()`,是因为其作用就是*在有任何修改时都去更新保存了设置的文件*。它的底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否更新成功。 -我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 对象转化成 JSON。有很大概率你已经用过了它!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? +我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 的对象转化成 JSON。大概率你已经用过它了!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? 噢,是的,就是有这么一个类,而且我们将要用上它!在协议扩展中,先写上如下代码: @@ -129,7 +133,7 @@ func update() -> Bool { 如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先创建了一个 `PropertyListEncoder` 的类,然后调用了 `encode(_:)` 方法来对 `self`(遵循了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! -如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里我们并未对异常做任何额外操作,我们只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 +如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里并未对异常做任何额外操作,只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 上面的代码并没有将编码后的数据*写入*文件。在 `do` 中,再加上这么一行: @@ -158,7 +162,7 @@ func update() -> Bool { } ``` -在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望方法内部所使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: +在继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望方法内部所使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: ```swift func update() throws @@ -212,7 +216,7 @@ mutating func load() -> Bool { } ``` -我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次需要使用 `path` 属性来获取路径字符串。当确定文件存在之后,就从文件中加载内容到 `fileContents` 实例中,拿到加载的数据后,通过 `PropertyListDecoder` 类解码后,对 `self`(遵循了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,返回 true,如果出现了错误则会抛出异常,然后将错误打印在控制台并且返回 false。 +我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次需要使用 `path` 属性来获取路径的字符串。当确定文件存在之后,就从文件中加载内容到 `fileContents` 实例中,拿到加载的数据后,通过 `PropertyListDecoder` 类解码后,对 `self`(遵循了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,返回 true,如果出现了错误则会抛出异常,然后将错误打印在控制台并且返回 false。 现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况还没有返回值。如果没有文件可以加载设置,那么就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! @@ -229,7 +233,7 @@ mutating func load() -> Bool { 现在方法在所有的情况下都有返回值了。如果是第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 ## 通过应用包中的 Plist 文件初始化设置 -既然我们一直在聊设置,那么很自然就会想到它们的的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: +既然我们一直在聊设置,那么很自然就会想到它们的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该有初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: *属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* @@ -283,10 +287,10 @@ do { } catch { ... } ``` -无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常工作。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 +无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常运行。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 ## 删除设置文件 -对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: +对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了达到这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: ```swift protocol SettingsManageable { @@ -315,7 +319,7 @@ func delete() -> Bool { ## 重置设置 如果有办法可以在任意时间切换回最初的设置,那将会是个很有用的功能。这背后的含义其实是删除缓存目录中的属性列表文件,然后再把初始值写回去。 -当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,当初始值被赋给了属性,事情就变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,又重新保存到文件中,这显然是错误的! +当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,初始值已经被赋值给了属性,这就让事情变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,然后又重新保存到文件中,这显然是错误的! 为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得是第一次在缓存目录创建的文件。我们将会以它来重置设置为初始值,并且这样的话,也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 @@ -374,7 +378,7 @@ mutating func load() -> Bool { } ``` -现在我们可以专注到重置方法的实现啦。在 `SettingsManageable` 协议中添加: +现在我们可以专注于重置方法的实现啦。在 `SettingsManageable` 协议中添加: ```swift protocol SettingsManageable { @@ -396,7 +400,7 @@ mutating func reset() -> Bool { } ``` -下一步要小心一点。为了让 `reset()` 方法与加载设置的方法独立开来。因此,我们要首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后调用 `load()` 方法来加载设置。 +下一步要小心一点。为了让 `reset()` 方法与加载设置的方法独立开来。因此,我们要首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的初始设置备份文件中恢复,然后调用 `load()` 方法来加载设置。 这就是上面所描述「场景」的代码: @@ -415,10 +419,10 @@ mutating func reset() -> Bool { } ``` -如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! +如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! # 将属性列表内容当作字典 -所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 +所有在 `SettingsManageable` 协议中的方法,都是为遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的次数,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: @@ -430,7 +434,7 @@ protocol SettingsManageable { } ``` -这个方法会返回一个范型为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件在被解码的过程中,发生了如无法解码这样糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 +这个方法会返回一个范型为 `[String: Any?]` 的字典,或者是 nil,避免属性列表文件在被解码的过程中发生了如无法解码这样糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个之前已经做过好几次了。同时还要把文件内容加载到 `Data` 对象中: @@ -460,7 +464,7 @@ let dictionary = try PropertyListSerialization.propertyList(from: fileContents, let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) as? [String: Any?] ``` -`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。自从有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就变小了。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。既然我们正在讨论属性列表,就可以顺便提及此方法。 +`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。自从有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就变小了。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。但既然我们正在讨论属性列表,就可以顺便提及此方法。 现在要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? @@ -469,7 +473,7 @@ let dictionary = try PropertyListSerialization.propertyList(from: fileContents, # SettingsManageable 协议实战 是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! -首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你马上会注意这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意,会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 我们需要做的第一件事就是让 `AppSettings` 类遵循 `SettingsManageable` 协议。不要忘了也要遵循 `Codable` 协议: @@ -479,7 +483,7 @@ class AppSettings: Codable, SettingsManageable { } ``` -因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过之前实现的 `load()` 方法来加载设置: +因为我们一直讨论的是应用的设置,那就有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过之前实现的 `load()` 方法来加载设置: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -520,11 +524,11 @@ if AppSettings.shared.update() { print(AppSettings.shared.settingsURL().path) ``` -如果你运行应用,你就能在控制台中看到如下输出: +如果你运行应用,就能在控制台中看到如下输出: ![console](https://www.appcoda.com/wp-content/uploads/2019/08/t67_3_results_1.png) -如果你通过 Finder 跳转到打印出来的路径,你还能看到有两个文件被创建: +如果你通过 Finder 跳转到打印出来的路径,你还能看到创建了两个文件: ![files_created](https://www.appcoda.com/wp-content/uploads/2019/08/t67_4_files1.png) @@ -603,6 +607,6 @@ if let dictionary = playerSettings.toDictionary() { ![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) # 总结 -我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能以及提供所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! +我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能,以及协议如何提供了所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以再想想如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! 作为参考,你可以在 GitHub 上下载到 [完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From 095252255295cfd93008f92960696bc5c985ac96 Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Fri, 14 Feb 2020 11:28:13 +0800 Subject: [PATCH 07/20] update swift-protocols-app-configuration(v4) --- ...91220_swift-protocols-app-configuration.md | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index 8016ab32..4a4261c2 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -21,7 +21,7 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 > 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是遵循了该协议。 -用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议遵循类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 +简而言之,一个 Swift 协议定义了一些方法和属性,需要由协议遵循类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 @@ -29,18 +29,18 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! -通过协议定义一系列负责文件处理的方法,以及通过协议扩展提供这些方法的默认实现。这样一来每一个遵循了协议的类或结构体,都能够获得相同的文件相关的功能。不仅如此,支持了各种设置的类或结构体还能以这种方式同时存在于应用中,这些数据也能被分别处理。换句话说,这种协议就像是某种即插即用机制,可以使任何遵循它的自定义类型具备保存和读取数据的能力。 +通过协议定义一系列负责文件处理的方法,以及通过协议扩展提供这些方法的默认实现。这样一来每一个遵循了协议的类或结构体,都能够获得相同的文件相关的功能!不仅如此,支持了各种设置的类或结构体还能以这种方式同时存在于应用中,这些数据也能被分别处理。换句话说,这种协议就像是某种即插即用机制,可以使任何遵循它的自定义类型具备保存和读取数据的能力。 这是我长期以来都在使用的解决方案,现在是时候将它拿出来在这讨论了。这篇文章所讨论的内容很显然是针对 Swift 开发新人的,可以让他们了解协议,以及协议的实际用法。毫无疑问高级开发者们早已有了类似的解决方法,但无论你处于什么阶段,都推荐继续阅读下去。 -# 线路图 -我们今天的目标是创建一个可以被任何类型遵循,支持简单的保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +## 线路图 +今天的目标是创建一个协议,让遵循它的类型能以文件的形式很方便的读取或写入。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里处理的是属性列表数据,但也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型,比如 JSON 或是纯文本。 最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议对其改造。在你下载之后,请用 Xcode 打开然后往下阅读。 -# 开始:创建协议 +## 开始:创建协议 让我们打开启动项目,然后创建一个用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 *Create* 按钮,来让 Xcode 真正地创建新文件。 @@ -69,10 +69,10 @@ extension SettingsManageable where Self: Codable { 这样可以避免有的类型没有遵循 `Codable`,但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 -# 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! +## 定义并且实现协议的要求 +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! -## 获取文件 URL +### 获取文件 URL 任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! 向协议中添加下面方法的定义: @@ -94,11 +94,11 @@ extension SettingsManageable where Self: Codable { } ``` -第一行从缓存目录获取到了 URL。第二行将*遵循了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 +第一行从缓存目录获取到了 URL。第二行将**遵循了 `SettingsManageable` 自定义类型添加到了 URL 末尾**,还加上了 **“.plist” 扩展**,然后我们将其返回。 注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 -## 存储(更新)一个 SettingsManageable 实例 +### 存储(更新)一个 SettingsManageable 实例 毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 @@ -179,9 +179,9 @@ func update() throws { 可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了让你方便,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 -> 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html) 吧! +> 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://swiftgg.gitbook.io/swift/swift-jiao-cheng/17_error_handling) 吧! -## 通过文件加载设置 +### 通过文件加载设置 现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。我们要跟随的步骤很简单,只需要跟着下面这几步: 1. 检查文件是否存在。 @@ -232,7 +232,7 @@ mutating func load() -> Bool { 现在方法在所有的情况下都有返回值了。如果是第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 -## 通过应用包中的 Plist 文件初始化设置 +### 通过应用包中的 Plist 文件初始化设置 既然我们一直在聊设置,那么很自然就会想到它们的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该有初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: *属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* @@ -289,7 +289,7 @@ do { 无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常运行。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 -## 删除设置文件 +### 删除设置文件 对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了达到这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: ```swift @@ -316,7 +316,7 @@ func delete() -> Bool { 如果移除文件像上面这样成功了,那么方法会返回 true。如果在移除过程中产生了任何错误,那么返回 false。 -## 重置设置 +### 重置设置 如果有办法可以在任意时间切换回最初的设置,那将会是个很有用的功能。这背后的含义其实是删除缓存目录中的属性列表文件,然后再把初始值写回去。 当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,初始值已经被赋值给了属性,这就让事情变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,然后又重新保存到文件中,这显然是错误的! @@ -421,8 +421,8 @@ mutating func reset() -> Bool { 如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! -# 将属性列表内容当作字典 -所有在 `SettingsManageable` 协议中的方法,都是为遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的次数,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 +## 将属性列表内容当作字典 +上述这些 SettingsManageable 协议的方法,为遵循了协议的类或结构体提供了保存与加载的能力。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的次数,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,让我们来更进一步地使用它,看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: @@ -470,10 +470,10 @@ let dictionary = try PropertyListSerialization.propertyList(from: fileContents, 嘿,我将这个问题留给你作为可选的练习。给一个小提示,如果是我的话,会去找一个叫 `data(fromPropertyList:format:options)` 的方法。 -# SettingsManageable 协议实战 +## SettingsManageable 协议实战 是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! -首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你马上会注意这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意,会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。你会注意到,这个类有一个静态的共享实例,以及一个私有的初始化方法,它符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的特性。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意,会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 我们需要做的第一件事就是让 `AppSettings` 类遵循 `SettingsManageable` 协议。不要忘了也要遵循 `Codable` 协议: @@ -606,7 +606,7 @@ if let dictionary = playerSettings.toDictionary() { ``` ![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) -# 总结 +## 总结 我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能,以及协议如何提供了所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以再想想如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! 作为参考,你可以在 GitHub 上下载到 [完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From 7e5f8d6233658111d1f99026086f2bf5405338d9 Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Fri, 21 Feb 2020 17:24:22 +0800 Subject: [PATCH 08/20] patch swift-protocols-app-configuration(v4) --- appcoda/20191220_swift-protocols-app-configuration.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index 4a4261c2..f0b646c5 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -25,7 +25,7 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 -如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 +如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://swiftgg.gitbook.io/swift/swift-jiao-cheng/21_protocols),你可以在那里发现有许多有趣的信息。 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! @@ -34,7 +34,7 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 这是我长期以来都在使用的解决方案,现在是时候将它拿出来在这讨论了。这篇文章所讨论的内容很显然是针对 Swift 开发新人的,可以让他们了解协议,以及协议的实际用法。毫无疑问高级开发者们早已有了类似的解决方法,但无论你处于什么阶段,都推荐继续阅读下去。 ## 线路图 -今天的目标是创建一个协议,让遵循它的类型能以文件的形式很方便的读取或写入。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +今天的目标是创建一个协议,让遵循它的类型能以文件的形式很方便的读取或写入。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,到最后完全实现所有功能。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里处理的是属性列表数据,但也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型,比如 JSON 或是纯文本。 @@ -70,7 +70,7 @@ extension SettingsManageable where Self: Codable { 这样可以避免有的类型没有遵循 `Codable`,但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 ## 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体自动地保存及加载自身。因此,我们可以将这些类和结构体作为设置或者偏好选项进行改动或是更新,而不需要做额外的变动。从现在起,我们将会定义协议中的每一个方法,并且会在协议扩展中提供对应的实现。话不多说,让我们开始吧! ### 获取文件 URL 任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! @@ -99,7 +99,7 @@ extension SettingsManageable where Self: Codable { 注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 ### 存储(更新)一个 SettingsManageable 实例 -毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: +毋庸置疑,上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 2. 写入文件 @@ -182,7 +182,7 @@ func update() throws { > 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://swiftgg.gitbook.io/swift/swift-jiao-cheng/17_error_handling) 吧! ### 通过文件加载设置 -现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。我们要跟随的步骤很简单,只需要跟着下面这几步: +现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。只需要照着以下几个步骤来就可以了: 1. 检查文件是否存在。 2. 将文件内容加载为 `Data` 对象。 From bfb0fd61d5ea4e67fbc1ad62c0b0322c2a02ec09 Mon Sep 17 00:00:00 2001 From: Nemocdz Date: Thu, 12 Mar 2020 18:04:41 +0800 Subject: [PATCH 09/20] update source --- ...20\347\253\231\350\256\260\345\275\225.md" | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git "a/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" "b/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" index 1cf893d4..ad3f6e8d 100644 --- "a/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" +++ "b/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" @@ -4,23 +4,23 @@ | 网站 | 最后检查时间 | 最后收录文章 | 备注 | | ------------------------------------------------------------ | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 作者:Mattt
网址:[NSHipster](https://nshipster.com/) | 2020.1.6 | 原文:[Objective-C Direct Methods](https://nshipster.com/direct/)
Issue: [#290](https://github.com/SwiftGGTeam/translation/issues/290) | 参考 [NSHipster 翻译说明](https://github.com/SwiftGGTeam/translation/blob/master/NSHipster%E7%BF%BB%E8%AF%91%E8%AF%B4%E6%98%8E.md) | -| 作者:Helge Hess
网址:[Always Right Institute](http://www.alwaysrightinstitute.com/) | 2020.1.6 | 原文:[Dynamic Environments ¶ SwiftUI RulesI](http://www.alwaysrightinstitute.com/swiftuirules//)
Issue: [#263](https://github.com/SwiftGGTeam/translation/issues/263) | | -| 作者:Erica Sadun
网址:[Erica Sadun](http://ericasadun.com/) | 2020.1.6 | 原文:[The Beauty of Swift 5 String Interpolation](https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/)
Issue: [#175](https://github.com/SwiftGGTeam/translation/issues/175) | 文章一般比较短,话痨,口语化,不太好翻译,挑有价值的即可 | -| 作者:Umberto Raimondi
网址:[uraimo](https://www.uraimo.com/) | 2020.1.6 | 原文:[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)
Issue:[#47](https://github.com/SwiftGGTeam/translation/issues/47) | 2018、2019、2020 都只更新了树莓派相关的内容 | -| 作者:Soroush Khanlou
网址:[KHANLOU](http://khanlou.com/) | 2019.1.6 | 原文:[Regexes vs Combinatorial Parsing](http://khanlou.com/2019/12/regex-vs-combinatorial-parsing/)
Issue:[#291](https://github.com/SwiftGGTeam/translation/issues/291) | | -| 作者:Russ Bishop
网址:[Russ Bishop](http://www.russbishop.net/) | 2020.1.6 | 原文:[Cross-process Rendering:Surfaces save us](http://www.russbishop.net/cross-process-rendering)
Issue:[#276](https://github.com/SwiftGGTeam/translation/issues/276) | 作者喜欢写副标题,将副标题以冒号形式接在主标题后面 | -| 作者:待定
网址:[AppCoda](https://www.appcoda.com/category/tutorials/ios/) | 2020.1.6 | 原文:[A Complete Guide to In-App Purchases for iOS Development](https://www.appcoda.com/in-app-purchases-guide/)
Issue:[#277](https://github.com/SwiftGGTeam/translation/issues/277) | 注意每篇文章作者不一样,Swift 分类很久不更新,主要看 iOS 分类即可,另有 [台湾站](https://www.appcoda.com.tw),文章不全,翻译可做参考 | -| 作者:Arthur Knopper
网址:[IOSCREATOR](http://www.ioscreator.com/) | 2020.1.6 | 原文:[Add Event to Calendar iOS Tutorial](https://www.appcoda.com/arkit-face-tracking/)
Issue:[#176](https://github.com/SwiftGGTeam/translation/issues/176) | 文章很多,都是很初级的内容,挑新的有价值的即可 | -| 作者:Nick Hanan
网址:[Coding Explorer Blog](http://www.codingexplorer.com/) | 2020.1.6 | 原文:[Error Handling in Swift](https://www.codingexplorer.com/error-handling-swift/)
Issue:[#53](https://github.com/SwiftGGTeam/translation/issues/53) | 作者有时候会更新老文章,比如我们收录的这篇,2019 年又更新过一次,注意别重复添加 | -| 作者:Thomas Hanning
网址:[Thomas Hanning](http://www.thomashanning.com/) | 2020.1.6 | 原文:[Sets in Swift](http://www.thomashanning.com/sets-in-swift/)
Issue:[#156](https://github.com/SwiftGGTeam/translation/issues/156) | | -| 作者:Ole Begemann
网址:[Ole Begemann](http://oleb.net/blog/) | 2020.1.6 | 原文:[Chris Lattner on the origins of Swift](https://oleb.net/2019/chris-lattner-swift-origins/)
Issue:[#212](https://github.com/SwiftGGTeam/translation/issues/212) | | -| 作者:Olivier Halligon
网址:[Crunchy Development](http://alisoftware.github.io/) | 2020.1.6 | 原文:[StringInterpolation in Swift 5 — AttributedStrings](https://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/)
Issue:[#207](https://github.com/SwiftGGTeam/translation/issues/207) | 2019、2020 还没更新过 | -| 作者:Benedikt Terhechte
网址:[APPVENTURE](http://appventure.me/) | 2020.1.6 | 原文:[The usefulness of typealiases in swift](http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.htmll)
Issue:[#240](https://github.com/SwiftGGTeam/translation/issues/240) | | -| 作者:Mike Ash
网址:[NSBlog](https://www.mikeash.com/pyblog/) | 2020.1.6 | 原文:[objc_msgSend's New Prototype](https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html)
Issue: [#278](https://github.com/SwiftGGTeam/translation/issues/278) | 高质量博客,更新频率不高 | -| 作者:Dominik Hauser
网址:[Swift and Painless](http://swiftandpainless.com/) | 2020.1.6 | 原文:[#selector() and the responder chain](http://swiftandpainless.com/selector-and-the-responder-chain/)
Issue: [#49](https://github.com/SwiftGGTeam/translation/issues/49) | 2018、2019、2020 都没更新过 | -| 作者:Weston Hanners
网址:[FRESHLY SQUOZEN WOOJI JUICE](http://www.wooji-juice.com/blog/) | 2020.1.6 | 原文:[Stupid Swift Tricks #6: To Animate or Not to Animate](http://www.wooji-juice.com/blog/stupid-swift-tricks-6-animations.html)
Issue:[#163](https://github.com/SwiftGGTeam/translation/issues/163) | 2019、2020 还没更新过 | -| 作者:Tomasz Szulc
网址:[Tomasz Szulc](http://szulctomasz.com/programming-blog/) | 2020.1.6 | 原文:[Add fireworks and sparks to a UIView](http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/)
Issue:[#168](https://github.com/SwiftGGTeam/translation/issues/168) | 2019、2020 还没更新过 | -| 作者:Jameson Quave
网址:[JamesonQuave.com](http://jamesonquave.com/) | 2020.1.6 | 原文:[Core ML for iOS Apps in iOS 11](http://jamesonquave.com/blog/core-ml-for-ios-apps-in-ios-11/)
Issue:[#39](https://github.com/SwiftGGTeam/translation/issues/39) | 2018、2019 、2020 都没更新过 | -| 作者:Jacob Bandes-Storch
网址:[Jacob Bandes-Storch](http://bandes-stor.ch/archive/) | 2020.1.6 | 原文:[Help Yourself to Some Swift](https://bandes-stor.ch/blog/2015/11/28/help-yourself-to-some-swift/)
Issue:无 | 作者现在好像不写 Swift 相关文章了,很久没更新 | -| 作者:Natasha
网址:[Natasha the Robot](http://natashatherobot.com/) | 2020.1.6 | 原文:[Architecting for Features](https://www.natashatherobot.com/architecting-for-features/[#](https://github.com/SwiftGGTeam/translation/issues/))
Issue:无 | 2019、2020 还没更新过 | +| 作者:Mattt
网址:[NSHipster](https://nshipster.com/) | 2020.3.12 | 原文:[Xcode Build Configuration Files](https://nshipster.com/xcconfig/)
Issue: [#294](https://github.com/SwiftGGTeam/translation/issues/294) | 参考 [NSHipster 翻译说明](https://github.com/SwiftGGTeam/translation/blob/master/NSHipster%E7%BF%BB%E8%AF%91%E8%AF%B4%E6%98%8E.md) | +| 作者:Helge Hess
网址:[Always Right Institute](http://www.alwaysrightinstitute.com/) | 2020.3.12 | 原文:[Dynamic Environments ¶ SwiftUI RulesI](http://www.alwaysrightinstitute.com/swiftuirules//)
Issue: [#263](https://github.com/SwiftGGTeam/translation/issues/263) | | +| 作者:Erica Sadun
网址:[Erica Sadun](http://ericasadun.com/) | 2020.3.12 | 原文:[The Beauty of Swift 5 String Interpolation](https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/)
Issue: [#175](https://github.com/SwiftGGTeam/translation/issues/175) | 文章一般比较短,话痨,口语化,不太好翻译,挑有价值的即可 | +| 作者:Umberto Raimondi
网址:[uraimo](https://www.uraimo.com/) | 2020.3.12 | 原文:[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)
Issue:[#47](https://github.com/SwiftGGTeam/translation/issues/47) | 2018、2019、2020 都只更新了树莓派相关的内容 | +| 作者:Soroush Khanlou
网址:[KHANLOU](http://khanlou.com/) | 2020.3.12 | 原文:[Regexes vs Combinatorial Parsing](http://khanlou.com/2019/12/regex-vs-combinatorial-parsing/)
Issue:[#291](https://github.com/SwiftGGTeam/translation/issues/291) | | +| 作者:Russ Bishop
网址:[Russ Bishop](http://www.russbishop.net/) | 2020.3.12 | 原文:[Cross-process Rendering:Surfaces save us](http://www.russbishop.net/cross-process-rendering)
Issue:[#276](https://github.com/SwiftGGTeam/translation/issues/276) | 作者喜欢写副标题,将副标题以冒号形式接在主标题后面 | +| 作者:待定
网址:[AppCoda](https://www.appcoda.com/category/tutorials/ios/) | 2020.3.12 | 原文:[Understanding Higher Order Functions in Swift](https://www.appcoda.com/higher-order-functions-swift/)
Issue:[#295](https://github.com/SwiftGGTeam/translation/issues/295) | 注意每篇文章作者不一样,Swift 分类很久不更新,主要看 iOS 分类即可,另有 [台湾站](https://www.appcoda.com.tw),文章不全,翻译可做参考 | +| 作者:Arthur Knopper
网址:[IOSCREATOR](http://www.ioscreator.com/) | 2020.3.12 | 原文:[Add Event to Calendar iOS Tutorial](https://www.appcoda.com/arkit-face-tracking/)
Issue:[#176](https://github.com/SwiftGGTeam/translation/issues/176) | 文章很多,都是很初级的内容,挑新的有价值的即可 | +| 作者:Nick Hanan
网址:[Coding Explorer Blog](http://www.codingexplorer.com/) | 2020.3.12 | 原文:[Error Handling in Swift](https://www.codingexplorer.com/error-handling-swift/)
Issue:[#53](https://github.com/SwiftGGTeam/translation/issues/53) | 作者有时候会更新老文章,比如我们收录的这篇,2019 年又更新过一次,注意别重复添加 | +| 作者:Thomas Hanning
网址:[Thomas Hanning](http://www.thomashanning.com/) | 2020.3.12 | 原文:[Sets in Swift](http://www.thomashanning.com/sets-in-swift/)
Issue:[#156](https://github.com/SwiftGGTeam/translation/issues/156) | | +| 作者:Ole Begemann
网址:[Ole Begemann](http://oleb.net/blog/) | 2020.3.12 | 原文:[TopLevelEncoder and TopLevelDecoder in Combine](https://oleb.net/2020/topleveldecoder/)
Issue:[#296](https://github.com/SwiftGGTeam/translation/issues/296) | | +| 作者:Olivier Halligon
网址:[Crunchy Development](http://alisoftware.github.io/) | 2020.3.12 | 原文:[StringInterpolation in Swift 5 — AttributedStrings](https://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/)
Issue:[#207](https://github.com/SwiftGGTeam/translation/issues/207) | 2019、2020 还没更新过 | +| 作者:Benedikt Terhechte
网址:[APPVENTURE](http://appventure.me/) | 2020.3.12 | 原文:[The usefulness of typealiases in swift](http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.htmll)
Issue:[#240](https://github.com/SwiftGGTeam/translation/issues/240) | | +| 作者:Mike Ash
网址:[NSBlog](https://www.mikeash.com/pyblog/) | 2020.3.12 | 原文:[objc_msgSend's New Prototype](https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html)
Issue: [#278](https://github.com/SwiftGGTeam/translation/issues/278) | 高质量博客,更新频率不高 | +| 作者:Dominik Hauser
网址:[Swift and Painless](http://swiftandpainless.com/) | 2020.3.12 | 原文:[#selector() and the responder chain](http://swiftandpainless.com/selector-and-the-responder-chain/)
Issue: [#49](https://github.com/SwiftGGTeam/translation/issues/49) | 2018、2019、2020 都没更新过 | +| 作者:Weston Hanners
网址:[FRESHLY SQUOZEN WOOJI JUICE](http://www.wooji-juice.com/blog/) | 2020.3.12 | 原文:[STUPID SWIFT TRICKS #7:Writing a User Guide In It](http://www.wooji-juice.com/blog/stupid-swift-tricks-7-user-guide.htmll)
Issue:[#297](https://github.com/SwiftGGTeam/translation/issues/297) | 2019、2020 还没更新过 | +| 作者:Tomasz Szulc
网址:[Tomasz Szulc](http://szulctomasz.com/programming-blog/) | 2020.3.12 | 原文:[Add fireworks and sparks to a UIView](http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/)
Issue:[#168](https://github.com/SwiftGGTeam/translation/issues/168) | 2019、2020 还没更新过 | +| 作者:Jameson Quave
网址:[JamesonQuave.com](http://jamesonquave.com/) | 2020.3.12 | 原文:[Core ML for iOS Apps in iOS 11](http://jamesonquave.com/blog/core-ml-for-ios-apps-in-ios-11/)
Issue:[#39](https://github.com/SwiftGGTeam/translation/issues/39) | 2018、2019 、2020 都没更新过,2020 作者表示会回归 | +| 作者:Jacob Bandes-Storch
网址:[Jacob Bandes-Storch](http://bandes-stor.ch/archive/) | 2020.3.12 | 原文:[Help Yourself to Some Swift](https://bandes-stor.ch/blog/2015/11/28/help-yourself-to-some-swift/)
Issue:无 | 作者现在好像不写 Swift 相关文章了,很久没更新 | +| 作者:Natasha
网址:[Natasha the Robot](http://natashatherobot.com/) | 2020.3.12 | 原文:[Architecting for Features](https://www.natashatherobot.com/architecting-for-features/[#](https://github.com/SwiftGGTeam/translation/issues/))
Issue:无 | 2019、2020 还没更新过 | From 1342c1fa07bfdb7e288185cc213afbbf7f4c6138 Mon Sep 17 00:00:00 2001 From: Jianing Wang Date: Sat, 11 Apr 2020 11:09:06 +0800 Subject: [PATCH 10/20] =?UTF-8?q?=E7=BF=BB=E8=AF=91=EF=BC=9A=E3=80=90?= =?UTF-8?q?=E4=B8=AD=E3=80=91The=20usefulness=20of=20typealiases=20in=20sw?= =?UTF-8?q?ift=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 翻译:【中】The usefulness of typealiases in swift * First revision: the usefulness of typealiases in swift * Second revision: the usefulness of typealiases in swift * Third revision: the usefulness of typealiases in swift --- ..._the-usefulness-of-typealiases-in-swift.md | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 appventure/20190515_the-usefulness-of-typealiases-in-swift.md diff --git a/appventure/20190515_the-usefulness-of-typealiases-in-swift.md b/appventure/20190515_the-usefulness-of-typealiases-in-swift.md new file mode 100644 index 00000000..7efaf584 --- /dev/null +++ b/appventure/20190515_the-usefulness-of-typealiases-in-swift.md @@ -0,0 +1,303 @@ +title: "Swift 中类型别名的用途" +date: 2019-11-17 +tags: [Swift] +categories: [APPVENTURE] +permalink: 2019-05-15-the-usefulness-of-typealiases +keywords: typealias +custom_title: Swift 中类型别名的用途 +description: 本文描述了类型别名的巧妙用途 + +--- + +原文链接=http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.html +作者=Benedikt Terhechte +原文日期=2019-05-15 +译者=Ji4n1ng +校对= +定稿= + + + +## 什么是 `typealias`? + +当我们回忆那些 Swift 强大的语言特性时,很少有人会首先想到 `typealias`。然而,许多情况下类型别名会很有用。本文将简要介绍 `typealias` 是什么,如何定义它,并列出多个示例说明如何在自己的代码中使用它们。让我们开始深入了解吧! + + + +顾名思义,`typealias` 是特定类型的别名。类型,例如 `Int`、`Double`、`UIViewController` 或一种自定义类型。`Int32` 和 `Int8` 是不同的类型。换句话说,类型别名在你的代码库里插入现有类型的另一个名称。例如: + +```swift +typealias Money = Int +``` + +为 `Int` 类型创建别名。这样就可以在代码中的任何地方使用 `Money`,就像是 `Int` 一样: + +```swift +struct Bank { + typealias Money = Int + private var credit: Money = 0 + mutating func deposit(amount: Money) { + credit += amount + } + mutating func withdraw(amount: Money) { + credit -= amount + } +} +``` + +上面有一个结构体 `Bank` 来管理钱。但是,没有使用 `Int` 作为金额,而是使用 `Money` 类型。可以看出 `+=` 和 `-=` 运算符仍然可以按预期工作。 + +还可以混合使用类型别名和原始类型,以及匹配二者。可以这么做是因为对于 Swift 编译器来说,它们都解析为同一个东西: + +```swift +struct Bank { + typealias DepositMoney = Int + typealias WithdrawMoney = Int + private var credit: Int = 0 + mutating func deposit(amount: DepositMoney) { + credit += amount + } + mutating func withdraw(amount: WithdrawMoney) { + credit -= amount + } +} +``` + +在这里,我们混合使用了 `Int` 及其不同自定义类型别名 `DepositMoney` 和 `WithdrawMoney`。 + +### 泛型类型别名 + +除上述内容外,类型别名也可以具有泛型参数: + +```swift +typealias MyArray = Array +let newArray: MyArray = MyArray(arrayLiteral: 1, 2, 3) +``` + +上面,为 `MyArray` 定义了一个类型别名,该别名与常规数组一样。最后,类型别名的泛型参数甚至可以具有约束。想象一下,我们希望新的 `MyArray` 只保留遵循 `StringProtocol` 的类型: + +```swift +typealias MyArray = Array where T: StringProtocol +``` + +这是一个不错的特性,你可以快速为特定类型定义数组,而不必将 `Array` 子类化。说到这里,让我们看一下类型别名的一些实践应用。 + +## 实践应用 + +### 更清晰的代码 + +第一个,同时也显而易见的用例,我们已经简要介绍过了。类型别名可以使代码更具含义。在 `typealias Money = Int` 示例中,我们引入了 `Money` 类型——一个清晰的概念。像 `let amount: Money = 0` 这样来使用它,比 `let amount: Int = 0` 更容易理解。在第一个示例中,你立刻就明白这是*金钱*的*数额*。而在第二个示例中,它可以是任何东西:自行车的数量、字符的数量、甜甜圈的数量——这谁知道! + +这显然不是都必要的。如果函数签名已经清楚地说明了参数的类型(`func orderDonuts(amount: Int)`),那么包含其他的类型别名将是不必要的开销。另一方面,对于变量和常量来说,它通常可以提高可读性并极大地帮助编写文档。 + +### 更简单的可选闭包 + +Swift 中的可选闭包有点笨拙。接受一个 `Int` 参数并返回 `Int` 的闭包的常规定义如下所示: + +```swift +func handle(action: (Int) -> Int) { ... } +``` + +现在,如果要使此闭包为可选型,则不能仅添加问号: + +```swift +func handle(action: (Int) -> Int?) { ... } +``` + +毕竟,这不是一个可选型的闭包,而是*一个返回可选 `Int`* 的闭包。正确的方法是添加括号: + +```swift +func handle(action: ((Int) -> Int)?) { ... } +``` + +如果有多个这样的闭包,这将变得尤为难看。下面,有一个函数,它可以处理成功和失败情况,以及随着操作的进行调用一个附加的闭包。 + +```swift +func handle(success: ((Int) -> Int)?, + failure: ((Error) -> Void)?, + progress: ((Double) -> Void)?) { + +} +``` + +这小段代码包含*很多*括号。由于我们不打算成为 lisper(译者注:lisp 语言使用者),因此想通过对不同的闭包使用类型别名来解决此问题: + +```swift +typealias Success = (Int) -> Int +typealias Failure = (Error) -> Void +typealias Progress = (Double) -> Void + +func handle2(success: Success?, failure: Failure?, progress: Progress?) { ... } +``` + +实际上,这个函数看起来确实更具可读性。虽然这很好,但我们确实通过使用三行 `typealias` 引入了其他语法。但是,从长远来看,这实际上可能对我们有帮助,就像我们将在接下来看到的。 + +### 集中定义 + +这些特定类型不仅仅可以用在前面示例的那些操作处理器中。下面是经过略微修改,更符合实际使用的操作处理器类: + +```swift +final class Dispatcher { + private var successHandler: ((Int) -> Void)? + private var errorHandler: ((Error) -> Void)? + + func handle(success: ((Int) -> Void)?, error: ((Error) -> Void)?) { + self.successHandler = success + self.errorHandler = error + internalHandle() + } + + func handle(success: ((Int) -> Void)?) { + self.successHandler = success + internalHandle() + } + + func handle(error: ((Int)-> Void?)) { + self.errorHandler = error + internalHandle() + } + + private func internalHandle() { + ... + } +} +``` + +该结构体引入了两个闭包,一个用于成功情况,一个用于错误情况。但是,我们还希望提供更方便的函数,调用其中一个处理器即可。在上面的示例中,如果要向成功和错误处理器添加另一个参数(例如 `HTTPResponse`),那么需要更改很多代码。在三个地方,`((Int) -> Void)?` 需要变成 `((Int, HTTPResponse) -> Void)?`。错误处理器也是一样的。通过使用多个类型别名,可以避免这种情况,只需要在一个地方修改类型: + +```swift +final class Dispatcher { + typealias Success = (Int, HTTPResponse) -> Void + typealias Failure = (Error, HTTPResponse) -> Void + + private var successHandler: Success? + private var errorHandler: Failure? + + func handle(success: Success?, error: Failure?) { + self.successHandler = success + self.errorHandler = error + internalHandle() + } + + func handle(success: Success?) { + self.successHandler = success + internalHandle() + } + + func handle(error: Failure?) { + self.errorHandler = error + internalHandle() + } + + private func internalHandle() { + ... + } +} +``` + +这不仅易于阅读,而且随着在更多地方使用该类型,它也会继续发挥它的作用。 + +### 泛型别名 + +类型别名也可以是泛型的。一个简单的用例是强制执行具有特殊含义的容器。假设我们有一个处理图书的应用。一本书由章节组成,章节由页面组成。从根本上讲,这些只是数组。下面是 `typealias`: + +```swift +struct Page {} +typealias Chapter = Array +typealias Book = Array +``` + +与仅使用数组相比,这有两个好处。 + +1. 该代码更具解释性。 +2. 包装页面的数组*只*能包含页面,而不能包含其它的。 + +回顾我们先前使用*成功*和*失败*处理程序的示例,我们可以通过使用泛型处理程序来进一步改进: + +```swift +typealias Handler = (In, HTTPResponse?, Context) -> Void + +func handle(success: Handler?, + failure: Handler?, + progress: Handler?,) +``` + +这样的组合确实非常棒。这使我们能够编写一个更简单的函数,并可以在一个地方编辑 `Handler`。 + +这种方法对于自定义的类型也非常有用。你可以创建一个泛型定义,然后定义详细的类型别名: + +```swift +struct ComputationResult { + private var result: T +} + +typealias DataResult = ComputationResult +typealias StringResult = ComputationResult +typealias IntResult = ComputationResult +``` + +再说一遍,类型别名允许我们编写更少的代码并简化代码中的定义。 + +### 像函数一样的元组 + +同样,可以使用泛型和元组来定义类型,而不是必须用结构体。下面,我们设想了一种遗传算法的数据类型,它可以在多代中修改其值 `T`。 + +```swift +typealias Generation = (initial: T, seed: T, count: Int, current: T) +``` + +如果定义这样的类型别名,则实际上可以像初始化一个结构体那样对其进行初始化: + +```swift +let firstGeneration = Generation(initial: 10, seed: 42, count: 0, current: 10) +``` + +尽管它看起来确实像一个结构体,但它只是一个元组的类型别名。 + +### 组合协议 + +有时,你会遇到一种情况,你有多个协议,而且需要使用一个特定类型来把这些协议都实现。这种情况通常发生在当你定义了一个协议层来提高灵活性时。 + +```swift +protocol CanRead {} +protocol CanWrite {} +protocol CanAuthorize {} +protocol CanCreateUser {} + +typealias Administrator = CanRead & CanWrite & CanAuthorize & CanCreateUser + +typealias User = CanRead & CanWrite + +typealias Consumer = CanRead +``` + +在这里,我们定义了权限层。管理员可以做所有事情,用户可以读写,而消费者只能读。 + +### 关联类型 + +这超出了本文的范围,但是协议的关联类型也可以通过类型别名来定义: + +```swift +protocol Example { + associatedtype Payload: Numeric +} + +struct Implementation: Example { + typealias Payload = Int +} +``` + +## 缺点 + +尽管类型别名是一个非常有用的功能,但它们有一个小缺点:如果你不熟悉代码库,那么对下面这两个定义的理解会有很大区别。 + +```swift +func first(action: (Int, Error?) -> Void) {} +func second(action: Success) {} +``` + +第二个不是立即就能明白的。`Success` 是什么类型?如何构造它?你必须在 Xcode 中按住 Option 单击它,以了解它的功能和工作方式。这会带来额外的工作量。如果使用了许多类型别名,则将花费更多的时间。这没有很好的解决方案,(通常)只能依赖于用例。 + +## 最后 + +我希望你能喜欢这篇关于类型别名可能性的小总结。如果你有任何反馈意见,[可以在 Twitter 上找到我](https://twitter.com/terhechte)。 \ No newline at end of file From 580a8691b9e2baed19bce8fbbb0a7bcb4c1972b7 Mon Sep 17 00:00:00 2001 From: Nemocdz Date: Sat, 25 Apr 2020 23:44:55 +0800 Subject: [PATCH 11/20] fix code style --- appcoda/20190806_layout-feedback-loop.md | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/appcoda/20190806_layout-feedback-loop.md b/appcoda/20190806_layout-feedback-loop.md index 9fe4a26c..626916f1 100644 --- a/appcoda/20190806_layout-feedback-loop.md +++ b/appcoda/20190806_layout-feedback-loop.md @@ -47,7 +47,7 @@ Apple 提供了很多方法来解决这类问题: 让我们回顾一下符号断点是如何工作的:它会计算 `layoutSubviews()` 的调用次数并在单个 run loop 迭代中超过某个临界值时发送一个事件。听起来很简单,对吧? -``` +```swift class TrackableView: UIView { var counter: Int = 0 @@ -70,7 +70,7 @@ class TrackableView: UIView { 让我们开始吧——我们将创建自定义子类,并将原始视图的类更改为新的子类: -``` +```swift struct LayoutLoopHunter { struct RuntimeConstants { @@ -97,6 +97,7 @@ struct LayoutLoopHunter { ``` `objc_allocateClassPair()` 方法的文档告诉我们这个方法何时失败: + > 新类,或者如果无法创建类,则为 Nil (例如,所需名称已被使用)。 这就意味着不能拥有两个同名的类。我们的策略是为单个视图类创建一个单独的运行时类。这就是我们在原始类名前加上前缀来形成新类的名称的原因。 @@ -108,7 +109,7 @@ struct LayoutLoopHunter { 但是目前,**只有一个方法奏效**。你可以想象属性是存储在分配给类的内存里的东西,然而关联对象则储存在一个完全不同的地方。因为分配给已存在对象的内存是固定的,所以我们在自定义类上新添加的属性将会从其他资源里“窃取”内存。它可能导致意料之外的行为和难以调试的程序崩溃(点击 [这里](https://stackoverflow.com/questions/3346427/object-setclass-to-bigger-class) 查看更多信息)。但是在使用关联对象的情况下,它们将会存储在运行时创建的一个哈希表里,这是完全安全的。 -``` +```swift static var CounterKey = "_counter" ... @@ -118,7 +119,7 @@ objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, 0, .OBJC_ 当新的子类被创建时,计数器初值设置为 0。接下来,让我们实现这个新的`layoutSubviews()` 方法,并将它添加到我们的类中: -``` +```swift let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in guard let _self = nullableSelf else { return } @@ -137,7 +138,7 @@ class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), impleme 为了理解上面这段代码实际上在干什么,让我们看一下这个来自 `` 的结构体: -``` +```swift struct objc_method { SEL method_name; char *method_types; @@ -152,16 +153,16 @@ struct objc_method { 你可以把 Witness Table(在其他编程语言中,它也被称作方法派发表)想象成一个简单的字典数据结构。那么选择器为键,且实现部分则为对应的值。 在下面这行代码中: -``` +```swift class_addMethod(trackableClass,#selector(originalClass.layoutSubviews), implementation, "v@:") -``` +``` 我们所做的是给 `layoutSubviews()` 方法对应的键分配新值。 这个方法直截了当。我们获得这个计数器,使它的计数值加一。如果计数值超过临界值,我们会发送分析事件,其中包含类名和想要的任何数据体。 让我们回顾一下如何对关联对象实现和使用键: -``` +```swift static var CounterKey = “_counter” ... @@ -170,7 +171,7 @@ objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, counter+1 为什么我们使用 `var` 来修饰计数器的键这个静态属性并在传递到其他地方时使用引用?答案隐藏在 Swift 语言基础——字符串之中。字符串像其他所有的值类型一样,是按值传递的。那么,当你把它传入这个闭包时,这个字符串将会被复制到一个不同的地址,这会导致在关联对象表中产生一个完全不同的键。`&` 符号总是保证将相同的地址作为键参数的值。你可以尝试以下代码: -``` +```swift func printAddress(_ string: UnsafeRawPointer) { print("\(string)") } @@ -194,7 +195,7 @@ closure() 为了避免发生一些难以预料的布局行为,我们得调用父类的实现,但这不像平常那样简单明了: -``` +```swift let selector = #selector(originalClass.layoutSubviews) let originalImpl = class_getMethodImplementation(originalClass, selector) @@ -211,7 +212,7 @@ originalLayoutSubviews(view, selector) 目前为止,让我们看看实现部分: -``` +```swift static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) { // 我们根据功能的前缀和原始类名为新类创建名称 let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))” @@ -261,7 +262,7 @@ static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () - ``` 让我们为视图创建模拟布局循环,并为其设置计数器来进行测试: -``` +```swift class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() @@ -284,7 +285,7 @@ class ViewController: UIViewController { 我们该怎么解决这个问题呢?只需在每次 run loop 迭代时重置计数器。为了做到这一点,我们可以创建一个 [**DispatchWorkItem**](https://www.appcoda.com/grand-central-dispatch/),它重置计数器,并在主队列上异步传递它。通过这种方式,它会在 run loop 下一次进入主线程时被调用: -``` +```swift static var ResetWorkItemKey = “_resetWorkItem” ... @@ -302,7 +303,7 @@ objc_setAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey, currentResetW 最终的代码: -``` +```swift struct LayoutLoopHunter { struct RuntimeConstants { From 0ab088137da49f097de6cc2c1b7f9a917cc64409 Mon Sep 17 00:00:00 2001 From: Nemocdz Date: Mon, 11 May 2020 23:48:08 +0800 Subject: [PATCH 12/20] update source --- ...20\347\253\231\350\256\260\345\275\225.md" | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git "a/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" "b/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" index ad3f6e8d..05c58db7 100644 --- "a/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" +++ "b/\346\235\245\346\272\220\347\253\231\350\256\260\345\275\225.md" @@ -4,23 +4,23 @@ | 网站 | 最后检查时间 | 最后收录文章 | 备注 | | ------------------------------------------------------------ | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 作者:Mattt
网址:[NSHipster](https://nshipster.com/) | 2020.3.12 | 原文:[Xcode Build Configuration Files](https://nshipster.com/xcconfig/)
Issue: [#294](https://github.com/SwiftGGTeam/translation/issues/294) | 参考 [NSHipster 翻译说明](https://github.com/SwiftGGTeam/translation/blob/master/NSHipster%E7%BF%BB%E8%AF%91%E8%AF%B4%E6%98%8E.md) | -| 作者:Helge Hess
网址:[Always Right Institute](http://www.alwaysrightinstitute.com/) | 2020.3.12 | 原文:[Dynamic Environments ¶ SwiftUI RulesI](http://www.alwaysrightinstitute.com/swiftuirules//)
Issue: [#263](https://github.com/SwiftGGTeam/translation/issues/263) | | -| 作者:Erica Sadun
网址:[Erica Sadun](http://ericasadun.com/) | 2020.3.12 | 原文:[The Beauty of Swift 5 String Interpolation](https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/)
Issue: [#175](https://github.com/SwiftGGTeam/translation/issues/175) | 文章一般比较短,话痨,口语化,不太好翻译,挑有价值的即可 | -| 作者:Umberto Raimondi
网址:[uraimo](https://www.uraimo.com/) | 2020.3.12 | 原文:[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)
Issue:[#47](https://github.com/SwiftGGTeam/translation/issues/47) | 2018、2019、2020 都只更新了树莓派相关的内容 | -| 作者:Soroush Khanlou
网址:[KHANLOU](http://khanlou.com/) | 2020.3.12 | 原文:[Regexes vs Combinatorial Parsing](http://khanlou.com/2019/12/regex-vs-combinatorial-parsing/)
Issue:[#291](https://github.com/SwiftGGTeam/translation/issues/291) | | -| 作者:Russ Bishop
网址:[Russ Bishop](http://www.russbishop.net/) | 2020.3.12 | 原文:[Cross-process Rendering:Surfaces save us](http://www.russbishop.net/cross-process-rendering)
Issue:[#276](https://github.com/SwiftGGTeam/translation/issues/276) | 作者喜欢写副标题,将副标题以冒号形式接在主标题后面 | -| 作者:待定
网址:[AppCoda](https://www.appcoda.com/category/tutorials/ios/) | 2020.3.12 | 原文:[Understanding Higher Order Functions in Swift](https://www.appcoda.com/higher-order-functions-swift/)
Issue:[#295](https://github.com/SwiftGGTeam/translation/issues/295) | 注意每篇文章作者不一样,Swift 分类很久不更新,主要看 iOS 分类即可,另有 [台湾站](https://www.appcoda.com.tw),文章不全,翻译可做参考 | -| 作者:Arthur Knopper
网址:[IOSCREATOR](http://www.ioscreator.com/) | 2020.3.12 | 原文:[Add Event to Calendar iOS Tutorial](https://www.appcoda.com/arkit-face-tracking/)
Issue:[#176](https://github.com/SwiftGGTeam/translation/issues/176) | 文章很多,都是很初级的内容,挑新的有价值的即可 | -| 作者:Nick Hanan
网址:[Coding Explorer Blog](http://www.codingexplorer.com/) | 2020.3.12 | 原文:[Error Handling in Swift](https://www.codingexplorer.com/error-handling-swift/)
Issue:[#53](https://github.com/SwiftGGTeam/translation/issues/53) | 作者有时候会更新老文章,比如我们收录的这篇,2019 年又更新过一次,注意别重复添加 | -| 作者:Thomas Hanning
网址:[Thomas Hanning](http://www.thomashanning.com/) | 2020.3.12 | 原文:[Sets in Swift](http://www.thomashanning.com/sets-in-swift/)
Issue:[#156](https://github.com/SwiftGGTeam/translation/issues/156) | | -| 作者:Ole Begemann
网址:[Ole Begemann](http://oleb.net/blog/) | 2020.3.12 | 原文:[TopLevelEncoder and TopLevelDecoder in Combine](https://oleb.net/2020/topleveldecoder/)
Issue:[#296](https://github.com/SwiftGGTeam/translation/issues/296) | | -| 作者:Olivier Halligon
网址:[Crunchy Development](http://alisoftware.github.io/) | 2020.3.12 | 原文:[StringInterpolation in Swift 5 — AttributedStrings](https://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/)
Issue:[#207](https://github.com/SwiftGGTeam/translation/issues/207) | 2019、2020 还没更新过 | -| 作者:Benedikt Terhechte
网址:[APPVENTURE](http://appventure.me/) | 2020.3.12 | 原文:[The usefulness of typealiases in swift](http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.htmll)
Issue:[#240](https://github.com/SwiftGGTeam/translation/issues/240) | | -| 作者:Mike Ash
网址:[NSBlog](https://www.mikeash.com/pyblog/) | 2020.3.12 | 原文:[objc_msgSend's New Prototype](https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html)
Issue: [#278](https://github.com/SwiftGGTeam/translation/issues/278) | 高质量博客,更新频率不高 | -| 作者:Dominik Hauser
网址:[Swift and Painless](http://swiftandpainless.com/) | 2020.3.12 | 原文:[#selector() and the responder chain](http://swiftandpainless.com/selector-and-the-responder-chain/)
Issue: [#49](https://github.com/SwiftGGTeam/translation/issues/49) | 2018、2019、2020 都没更新过 | -| 作者:Weston Hanners
网址:[FRESHLY SQUOZEN WOOJI JUICE](http://www.wooji-juice.com/blog/) | 2020.3.12 | 原文:[STUPID SWIFT TRICKS #7:Writing a User Guide In It](http://www.wooji-juice.com/blog/stupid-swift-tricks-7-user-guide.htmll)
Issue:[#297](https://github.com/SwiftGGTeam/translation/issues/297) | 2019、2020 还没更新过 | -| 作者:Tomasz Szulc
网址:[Tomasz Szulc](http://szulctomasz.com/programming-blog/) | 2020.3.12 | 原文:[Add fireworks and sparks to a UIView](http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/)
Issue:[#168](https://github.com/SwiftGGTeam/translation/issues/168) | 2019、2020 还没更新过 | -| 作者:Jameson Quave
网址:[JamesonQuave.com](http://jamesonquave.com/) | 2020.3.12 | 原文:[Core ML for iOS Apps in iOS 11](http://jamesonquave.com/blog/core-ml-for-ios-apps-in-ios-11/)
Issue:[#39](https://github.com/SwiftGGTeam/translation/issues/39) | 2018、2019 、2020 都没更新过,2020 作者表示会回归 | -| 作者:Jacob Bandes-Storch
网址:[Jacob Bandes-Storch](http://bandes-stor.ch/archive/) | 2020.3.12 | 原文:[Help Yourself to Some Swift](https://bandes-stor.ch/blog/2015/11/28/help-yourself-to-some-swift/)
Issue:无 | 作者现在好像不写 Swift 相关文章了,很久没更新 | -| 作者:Natasha
网址:[Natasha the Robot](http://natashatherobot.com/) | 2020.3.12 | 原文:[Architecting for Features](https://www.natashatherobot.com/architecting-for-features/[#](https://github.com/SwiftGGTeam/translation/issues/))
Issue:无 | 2019、2020 还没更新过 | +| 作者:Mattt
网址:[NSHipster](https://nshipster.com/) | 2020.5.11 | 原文:[Contact Tracing](https://nshipster.com/contact-tracing/))
Issue: [#305](https://github.com/SwiftGGTeam/translation/issues/305) | 参考 [NSHipster 翻译说明](https://github.com/SwiftGGTeam/translation/blob/master/NSHipster%E7%BF%BB%E8%AF%91%E8%AF%B4%E6%98%8E.md) | +| 作者:Helge Hess
网址:[Always Right Institute](http://www.alwaysrightinstitute.com/) | 2020.5.11 | 原文:[Dynamic Environments ¶ SwiftUI RulesI](http://www.alwaysrightinstitute.com/swiftuirules//)
Issue: [#263](https://github.com/SwiftGGTeam/translation/issues/263) | | +| 作者:Erica Sadun
网址:[Erica Sadun](http://ericasadun.com/) | 2020.5.11 | 原文:[The Beauty of Swift 5 String Interpolation](https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/)
Issue: [#175](https://github.com/SwiftGGTeam/translation/issues/175) | 文章一般比较短,话痨,口语化,不太好翻译,挑有价值的即可 | +| 作者:Umberto Raimondi
网址:[uraimo](https://www.uraimo.com/) | 2020.5.11 | 原文:[All about Concurrency in Swift - Part 1: The Present](https://www.uraimo.com/2017/05/07/all-about-concurrency-in-swift-1-the-present/)
Issue:[#47](https://github.com/SwiftGGTeam/translation/issues/47) | 2018、2019、2020 都只更新了树莓派相关的内容 | +| 作者:Soroush Khanlou
网址:[KHANLOU](http://khanlou.com/) | 2020.5.11 | 原文:[Regexes vs Combinatorial Parsing](http://khanlou.com/2019/12/regex-vs-combinatorial-parsing/)
Issue:[#291](https://github.com/SwiftGGTeam/translation/issues/291) | | +| 作者:Russ Bishop
网址:[Russ Bishop](http://www.russbishop.net/) | 2020.5.11 | 原文:[Cross-process Rendering:Surfaces save us](http://www.russbishop.net/cross-process-rendering)
Issue:[#276](https://github.com/SwiftGGTeam/translation/issues/276) | 作者喜欢写副标题,将副标题以冒号形式接在主标题后面 | +| 作者:待定
网址:[AppCoda](https://www.appcoda.com/category/tutorials/ios/) | 2020.5.11 | 原文:[Understanding Higher Order Functions in Swift](https://www.appcoda.com/higher-order-functions-swift/)
Issue:[#295](https://github.com/SwiftGGTeam/translation/issues/295) | 注意每篇文章作者不一样,Swift 分类很久不更新,主要看 iOS 分类即可,另有 [台湾站](https://www.appcoda.com.tw),文章不全,翻译可做参考 | +| 作者:Arthur Knopper
网址:[IOSCREATOR](http://www.ioscreator.com/) | 2020.5.11 | 原文:[Add Event to Calendar iOS Tutorial](https://www.appcoda.com/arkit-face-tracking/)
Issue:[#176](https://github.com/SwiftGGTeam/translation/issues/176) | 文章很多,都是很初级的内容,挑新的有价值的即可 | +| 作者:Nick Hanan
网址:[Coding Explorer Blog](http://www.codingexplorer.com/) | 2020.5.11 | 原文:[Error Handling in Swift](https://www.codingexplorer.com/error-handling-swift/)
Issue:[#53](https://github.com/SwiftGGTeam/translation/issues/53) | 作者有时候会更新老文章,比如我们收录的这篇,2019 年又更新过一次,注意别重复添加 | +| 作者:Thomas Hanning
网址:[Thomas Hanning](http://www.thomashanning.com/) | 2020.5.11 | 原文:[Sets in Swift](http://www.thomashanning.com/sets-in-swift/)
Issue:[#156](https://github.com/SwiftGGTeam/translation/issues/156) | | +| 作者:Ole Begemann
网址:[Ole Begemann](http://oleb.net/blog/) | 2020.5.11 | 原文:[TopLevelEncoder and TopLevelDecoder in Combine](https://oleb.net/2020/topleveldecoder/)
Issue:[#296](https://github.com/SwiftGGTeam/translation/issues/296) | | +| 作者:Olivier Halligon
网址:[Crunchy Development](http://alisoftware.github.io/) | 2020.5.11 | 原文:[StringInterpolation in Swift 5 — AttributedStrings](https://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/)
Issue:[#207](https://github.com/SwiftGGTeam/translation/issues/207) | 2019、2020 还没更新过 | +| 作者:Benedikt Terhechte
网址:[APPVENTURE](http://appventure.me/) | 2020.5.11 | 原文:[The usefulness of typealiases in swift](http://appventure.me/posts/2019-5-15-the-usefulness-of-typealiases-in-swift.htmll)
Issue:[#240](https://github.com/SwiftGGTeam/translation/issues/240) | | +| 作者:Mike Ash
网址:[NSBlog](https://www.mikeash.com/pyblog/) | 2020.5.11 | 原文:[objc_msgSend's New Prototype](https://www.mikeash.com/pyblog/objc_msgsends-new-prototype.html)
Issue: [#278](https://github.com/SwiftGGTeam/translation/issues/278) | 高质量博客,更新频率不高 | +| 作者:Dominik Hauser
网址:[Swift and Painless](http://swiftandpainless.com/) | 2020.5.11 | 原文:[#selector() and the responder chain](http://swiftandpainless.com/selector-and-the-responder-chain/)
Issue: [#49](https://github.com/SwiftGGTeam/translation/issues/49) | 2018、2019、2020 都没更新过 | +| 作者:Weston Hanners
网址:[FRESHLY SQUOZEN WOOJI JUICE](http://www.wooji-juice.com/blog/) | 2020.5.11 | 原文:[STUPID SWIFT TRICKS #7:Writing a User Guide In It](http://www.wooji-juice.com/blog/stupid-swift-tricks-7-user-guide.htmll)
Issue:[#297](https://github.com/SwiftGGTeam/translation/issues/297) | | +| 作者:Tomasz Szulc
网址:[Tomasz Szulc](http://szulctomasz.com/programming-blog/) | 2020.5.11 | 原文:[Add fireworks and sparks to a UIView](http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/)
Issue:[#168](https://github.com/SwiftGGTeam/translation/issues/168) | 2019、2020 还没更新过 | +| 作者:Jameson Quave
网址:[JamesonQuave.com](http://jamesonquave.com/) | 2020.5.11 | 原文:[Tutorial: Night Mode Support in iOS 13 and Swift 5](https://jamesonquave.com/blog/tutorial-night-mode-support-in-ios-13-and-swift-5/)
Issue:[#306](https://github.com/SwiftGGTeam/translation/issues/406) | | +| 作者:Jacob Bandes-Storch
网址:[Jacob Bandes-Storch](http://bandes-stor.ch/archive/) | 2020.5.11 | 原文:[Help Yourself to Some Swift](https://bandes-stor.ch/blog/2015/11/28/help-yourself-to-some-swift/)
Issue:无 | 作者现在好像不写 Swift 相关文章了,很久没更新 | +| 作者:Natasha
网址:[Natasha the Robot](http://natashatherobot.com/) | 2020.5.11 | 原文:[Architecting for Features](https://www.natashatherobot.com/architecting-for-features/[#](https://github.com/SwiftGGTeam/translation/issues/))
Issue:无 | 2019、2020 还没更新过 | From 499395720277a339ad6a2e39c738f770ef30f741 Mon Sep 17 00:00:00 2001 From: ericchuhong Date: Sun, 24 May 2020 21:05:40 +0800 Subject: [PATCH 13/20] Ericchuhong patch 3 (#259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 【feature】新增翻译文件 * 【feature】根据梁大修改意见修改 * 【feature】修复部分翻译不够好的句子 * 【feature】根据思琪建议修改 Co-authored-by: 马楚鸿 <873324136@qq.com> --- nshipster/20190624_propertywrapper.md | 1296 +++++++++++++++++++++++++ 1 file changed, 1296 insertions(+) create mode 100644 nshipster/20190624_propertywrapper.md diff --git a/nshipster/20190624_propertywrapper.md b/nshipster/20190624_propertywrapper.md new file mode 100644 index 00000000..483ada0d --- /dev/null +++ b/nshipster/20190624_propertywrapper.md @@ -0,0 +1,1296 @@ +title: "属性修饰器" +date: +tags: [新特性] +categories: [Swift] +permalink: propertywrapper +keywords: 新特性,修饰器 +custom_title: 属性修饰器 +description: Swift 属性修饰器要让 SwiftUI 成为可能还有很长的路要走,但他们在塑造整个语言的未来方面可能发挥着更重要的作用。 + +--- +原文链接=https://nshipster.com/propertywrapper/ +作者=Mattt +原文日期=2019-06-24 +译者=ericchuhong +校对= +定稿= + + + +几年前,我们 [会说](https://nshipster.com/at-compiler-directives/) “at 符号”(`@`)——以及方括号和可笑的长方法名称——是 Objective-C 的特性,正如括号之于 [Lisp](https://en.wikipedia.org/wiki/Lisp_%28programming_language%29) 或者标点之于 [Perl](https://nshipster.com/assets/qbert-fe44c1a26bd163d2dfafa5334c7bfa7957c3c243cd0c19591f494a9cea9302dc.png)。 + +然后 Swift 来了,并用它来结束这些古怪小 🥨 图案一样的字形。或者说我们本以为会这样。 + + + +一开始,Swift 中的 `@` 只用在和 Objective-C 的混编中:`@IBAction`、`@NSCopying`、`@UIApplicationMain`等等。但之后 Swift 扩展出了越来越多的带有 `@` 前缀的 [属性](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html)。 + +我们在 [WWDC 2019](https://nshipster.com/wwdc-2019/) 上第一次看到了 Swift 5.1 和 SwiftUI 的同时公布。并且随着每一张“令人兴奋”的幻灯片出现了一个个前所未有的属性:`@State`、`@Binding`、`@EnvironmentObject`…… + +我们看到了Swift的未来,它充满了 `@` 符号。 + +--- + +等 SwiftUI 逐步成熟起来,我们才会深入介绍它。 + +本周,我们想仔细看看 SwiftUI 的一个关键语言特性——可能会对 Swift 5.1 之前版本产生最大影响的东西:*属性修饰器* + +--- + +## 关于 属性 ~~代理~~ ++修饰器++ + +属性修饰器是在 2019 年 3 月第一次 [在 Swift 论坛首次出现](https://forums.swift.org/t/pitch-property-delegates/21895)——SwiftUI 公布的前一个月。 + +在开始的时候,Swift 核心团队成员 Douglas Gregor 将它作为用户常用功能特性的一个统称(当时称为 *“属性代理”*),像有 `lazy` 关键字之类的。 + +懒惰是程序员的美德,这种普遍适用的功能是周到设计决策的特征,这让 Swift 成为一种很好用的语言。当一个属性被声明为 `lazy` 时,它推迟初始化其默认值,直到第一次访问才进行初始化。例如,你可以自己尝试实现这样的功能,使用一个私有属性,它需通过计算后才行被访问。而单单一个 `lazy` 关键字就可以让所有这些都变得没有必要。 + +```swift +struct <#Structure#> { + // 使用 lazy 关键字进行属性延迟初始化 + lazy var deferred = <#...#> + + // 没有 lazy 关键字的等效行为 + private var _deferred: <#Type#>? + var deferred: <#Type#> { + get { + if let value = _deferred { return value } + let initialValue = <#...#> + _deferred = initialValue + return initialValue + } + + set { + _deferred = newValue + } + } +} +``` + +[SE-0258: 属性修饰器](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) 目前正在进行第三次审核(预定于昨天结束,就在发布的时候), 并且承诺开放像 `lazy` 这样的功能,以便库作者可以自己实现类似的功能。 + +由于这个提案在其设计和实现上的阐述非常出色,我们这里就不做更多的解释了。我们不妨把重点放在别处,一起来看看这个功能为 Swift 带来了哪些新的可能——而且,在这个过程中,我们可以更好了解如何在项目使用这个新功能。 + +所以,供你参考,以下是新 `@propertyWrapper` 属性的四个潜在用例: + +- [约束值](#constraining-values) +- [转换属性赋值时的值](#transforming-values-on-property-assignment) +- [改变生成的等式和比较语义](#changing-synthesized-equality-and-comparison-semantics) +- [审查属性访问](#auditing-property-access) + +--- + + +## 约束值 + +SE-0258 提供了大量实用案例,包括了 `@Lazy`,`@Atomic`,`@ThreadSpecific` 和 `@Box`。但最让我们兴奋的是那个关于 `@Constrained` 的属性修饰器。 + +Swift 标准库提供了 [精确](https://en.wikipedia.org/wiki/IEEE_754)、高性能的浮点数类型,并且你可以拥有任何想要的精度——只要它是 [32](https://developer.apple.com/documentation/swift/float) 或 [64](https://developer.apple.com/documentation/swift/double)(或 [80](https://developer.apple.com/documentation/swift/float80))位长度。 + +如果你想要实现自定义浮点数类型,而且有强制要求有效值范围,这从 [Swift 3](https://github.com/apple/swift-evolution/blob/master/proposals/0067-floating-point-protocols.md) 开始已经成为可能。但是这样做需要遵循错综复杂的协议要求: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ExpressibleByIntegerLiteral + + + Float + + + ExpressibleByFloatLiteral + + + BinaryFloatingPoint + + + FloatingPoint + + + Float80 + + + Double + + + SignedNumeric + + + SignedInteger + + + Int + + + AdditiveArithmetic + + + Numeric + + + FixedWidthInteger + + + BinaryInteger + + + Comparable + + + Equatable + + + UnsignedInteger + + + UInt + + + + + + + + + + + + + + + + + + +来自:[航空学院的 Swift 数字指引](https://flight.school/books/numbers/) + +要把这么多协议实现下来工作量可不小,并且对于大多数用例,通常需要大量的工作来验证。 + +幸好,属性修饰器提供了一种将标准数字类型参数化的方式,同时又大大减少工作量。 + +### 实现一个限制值范围的属性修饰器 + +思考下面的 `Clamping` 结构。作为一个属性修饰器(由 `@propertyWrapper` 属性表示),它会自动在规定的范围内“限制”越界的值。 + +```swift +@propertyWrapper +struct Clamping { + var value: Value + let range: ClosedRange + + init(initialValue value: Value, _ range: ClosedRange) { + precondition(range.contains(value)) + self.value = value + self.range = range + } + + var wrappedValue: Value { + get { value } + set { value = min(max(range.lowerBound, newValue), range.upperBound) } + } +} +``` + +你可以使用 `@Clamping` 保证属性在转成模型 [化学溶液中的酸度](https://en.wikipedia.org/wiki/PH) 的过程中,处于 0-14 的常规范围内。 + +```swift +struct Solution { + @Clamping(0...14) var pH: Double = 7.0 +} + +let carbonicAcid = Solution(pH: 4.68) // 在标准情况下为 1 mM +``` + +如果尝试将 pH 值设定在限制的范围之外,将得到最接近的边界值(最小值或者最大值)来代替。 + +```swift +let superDuperAcid = Solution(pH: -1) +superDuperAcid.pH // 0 +``` + +你可以在其他属性修饰器的实现中使用属性修饰器。例如,这个 `UnitInterval` 属性修饰起器委托给 `@Clamping`,把值约束在 0 和 1 之间,包括 0 和 1。 + +```swift +@propertyWrapper +struct UnitInterval { + @Clamping(0...1) + var wrappedValue: Value = .zero + + init(initialValue value: Value) { + self.wrappedValue = value + } +} +``` + +再比如,你可以使用 `@UnitInterval` 属性修饰器定义一个 `RGB` 的类型,用来表示红色、绿色、蓝色的百分比强度。 + +```swift +struct RGB { + @UnitInterval var red: Double + @UnitInterval var green: Double + @UnitInterval var blue: Double +} + +let cornflowerBlue = RGB(red: 0.392, green: 0.584, blue: 0.929) +``` + +#### 举一反三 + +- 实现一个 `@Positive`/`@NonNegative` 属性装饰器,将无符号整数赋值成有符号整数类型。 +- 实现一个 `@NonZero` 属性装饰器,使得一个数值要么大于,要么小于 `0`。 +- `@Validated` 或者 `@Whitelisted`/`@Blacklisted` 属性装饰器,约束了什么样的值可以被赋值。 + + +## 转换属性赋值时的值 + +从用户接收文本输入是应用开发者经常头疼的问题。从无聊的字符串编码到恶意的文本字段注入攻击,开发者有太多事情需要注意。但在开发者面对的的问题中,最难以捉摸和令人困扰的是接收用户生成的内容,而且这些内容开头和结尾都带有空格。 + +在内容开头有一个单独的空格,可以让 URL 无效,也可以混淆日期解析器,还可能造成差一错误(off-by-one error): + +```swift +import Foundation + +URL(string: " https://nshipster.com") // nil (!) + +ISO8601DateFormatter().date(from: " 2019-06-24") // nil (!) + +let words = " Hello, world!".components(separatedBy: .whitespaces) +words.count // 3 (!) +``` + +说到用户输入,客户端经常以没留意做理由,然后把所有东西 *原原本本* 发送给服务器。`¯\_(ツ)_/¯`。 + +当然我不是在倡导客户端应该为此负责更多处理工作,这种情况就涉及到了 Swift 属性修饰器另外一个引人注目的用例。 + +--- + +Foundation 框架将 `trimmingCharacters(in:)` 方法桥接到了 Swift 的字符串中,除了一些其他作用以外,它提供了便利的方式来裁剪掉 `String` 值首位两端的空格。虽然可以通过调用这个方法来保证数据健全,但是还不够便利。如果你也有过类似的经历,你肯定会想知道有没有更好的方案。 + +或许你找到了一种较为通用的方法,通过 `willSet` 属性回调来寻解脱……唯一让人不能满意的是,这个方法无法改变已经发生的事情。 + +```swift +struct Post { + var title: String { + willSet { + title = newValue.trimmingCharacters(in: .whitespacesAndNewlines) + /* ⚠️ 尝试在它自己的 willSet 中存储属性 'title',该属性将会被新值覆盖*/ + } + } +} +``` + +从上面看,你可能想到可以用 `didSet`,作为解决问题的康庄大道……不过我想你马上就会想起来 Swift 里的一条规定,即 `didSet` 在属性初始化赋值时是不会被调用的。 + +```swift +struct Post { + var title: String { + // 😓 初始化期间未调用 + didSet { + self.title = title.trimmingCharacters(in: .whitespacesAndNewlines) + } + } +} +``` +> 在属性自己的 `didSet` 回调方法里面,很幸运不会再次触发回调,所以你不必担心意料之外的递归调用。 + +在你的坚持不懈下,你很可能用尽了一切办法......但回过头来,你发现其实并没有什么方法能够既满足人因工程学的标准,又满足性能方面的要求 + +如果你对此深有体会,那么恭喜你,你在这方面的探索可以到此为止了,因为属性装饰器将是这个问题的终极解决方案。 + +### 实现为字符串值裁截空格的属性修饰器 + +看下下面的 `Trimmed` 结构体,它从输入的字符串裁截了空格和换行。 + +```swift +import Foundation + +@propertyWrapper +struct Trimmed { + private(set) var value: String = "" + + var wrappedValue: String { + get { value } + set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } + } + + init(initialValue: String) { + self.wrappedValue = initialValue + } +} +``` + +下面的代码为 `Post` 结构中每个 `String` 属性标记了 `@Trimmed` ,通过这种方式,任何赋值给 `title` 或 `body` 的字符串值——无论是在初始化期间还是通过属性访问后——都将自动删除其开头或结尾的空格。 + +```swift +struct Post { + @Trimmed var title: String + @Trimmed var body: String +} + +let quine = Post(title: " Swift Property Wrappers ", body: "<#...#>") +quine.title // "Swift Property Wrappers" (no leading or trailing spaces!) + +quine.title = " @propertyWrapper " +quine.title // "@propertyWrapper" (still no leading or trailing spaces!) +``` + +#### 举一反三 + +- 实现一个 `@Transformed` 属性修饰器,它允许对输入的字符串进行 [ICU 转换](https://developer.apple.com/documentation/foundation/nsstring/1407787-applyingtransform)。 +- 实现一个 `@Normalized` 属性修饰器,它允许一个 `String` 属性自定义它[正规形式](https://unicode.org/reports/tr15/#Norm_Forms) +- 实现一个 `@Quantized`/`@Rounded`/`@Truncated` 属性修饰器,它会把数值转换到一种特定的精度(例如:向上舍入到最近的 ½ 精度),但是内部要关注到精确过程的中间值,防止连锁的舍入错误。 + + +## 改变生成的等式和比较语义 + +> 这个方式取决于遵循 synthesized 协议的实现细节,并且可能会在这个功能完成之前发生改变(尽管我们希望这个方法仍然像下面所说一样继续可用)。 + +在 Swift 中,两个 `String` 值如果他们 [*标准等价*](https://unicode.org/reports/tr15/#Canon_Compat_Equivalence) 就会被人认为是相等。在大多数情况下,Swift 字符串的比较方式与我们的预期一致:即两个字符串包含有相同的字符就会相等,不管它是一个合成字符,还是将这个合成字符拆解成多个字符——举个例子来说,就是“é”(`U+00E9 带有锐音的拉丁小写字母 E`)等于“e”(`U+0065 拉丁小写字母 E`)+“◌́”(`U+0301T 和锐音组合`)。 + +但是,如果你在特殊的情况下需要不同的相等语义呢?例如字符串相等的时候 *不区分大小写*? + +在今天,你可以使用许多方法,利用已有的语言特性解决这个问题: + +- 要完成这个功能,你可以在 `==` 比较的时候用 `lowercased()` 做一次处理,但和其他手动处理方式一样,这种方式容易出现人为的错误。 +- 你可以创建一个包含 `String` 值的自定义 `CaseInsensitive` 类型。但你必须要完成很多额外的工作,才能把它打磨的像标准的 `String` 类型一样即符合人因工程学的标准,又提供完全相同的功能。 +- 虽然你可以定义一个[自定义操作符](https://nshipster.com/swift-operators/#defining-custom-operators) 但又有什么操作符能比 `==` 更贴近相等的含义呢。 + +上面的方法并没有哪个能让人完全信服,还好在 Swift 5.1 中,属性修饰器的特性让我们拥有了一个完美的解决方案。 + +> 和文章开头提到状况一样(即实现一个自定义浮点数类型),Swift 采用面向协议的方式,将完成字符串的职责代理给一系列的更细粒度的类型(narrowly-defined types). + +对于好奇心强的读者,这里是一张关系图,里面展示了在 Swift 标准库中所有字符串类型之间的关系。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RangeReplaceableCollection + + + TextOutputStreamable + + + TextOutputStream + + + Collection + + + Sequence + + + BidirectionalCollection + + + String + + + StringProtocol + + + CustomDebugStringConvertible + + + LosslessStringConvertible + + + CustomStringConvertible + + + + ExpressibleByExtendedGraphemeClusterLiteral + + + ExpressibleByUnicodeScalarLiteral + + + ExpressibleByStringInterpolation + + + ExpressibleByStringLiteral + + + Comparable + + + Hashable + + + Equatable + + +来自:[航空学院的 Swift 字符串指引](https://flight.school/books/strings/) + +当你 *能够* 创建一个与 `String` 等价的自定义类型时,[文档](https://developer.apple.com/documentation/swift/stringprotocol) 却又强烈的建议不要这样做: + +> 不应该再有别的类型遵循 StringProtocol 。在标准库中应当只有 `String` 和 `Substring` 遵循它。 + +### 实现一个不区分大小写的属性修饰器 + +下面的 `CaseInsensitive` 类型实现了一个修饰 `String`/`SubString` 的属性修饰器。通过桥接 `NSString` 的 API [`caseInsensitiveCompare(_:)`](https://developer.apple.com/documentation/foundation/nsstring/1414769-caseinsensitivecompare) ,`CaseInsensitive` 类型符合了 `Comparable` 协议(本质是通过扩展的方式实现了 `Equatable` 协议): + +```swift +import Foundation + +@propertyWrapper +struct CaseInsensitive { + var wrappedValue: Value +} + +extension CaseInsensitive: Comparable { + private func compare(_ other: CaseInsensitive) -> ComparisonResult { + wrappedValue.caseInsensitiveCompare(other.wrappedValue) + } + + static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { + lhs.compare(rhs) == .orderedSame + } + + static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { + lhs.compare(rhs) == .orderedAscending + } + + static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { + lhs.compare(rhs) == .orderedDescending + } +} +``` + +> 虽然大于运算符(`>`)[可以被自动派生](https://nshipster.com/equatable-and-comparable/#comparable),我们为了优化性能应该在这里实现它,避免对底层方法 `caseInsensitiveCompare` 进行不必要的调用。 + +构造两个只是大小写不同的字符串,并且对于标准的相等检查他们会返回 `false`,但是在用 `CaseInsensitive` 对象修饰的时候返回 `true`。 + +```swift +let hello: String = "hello" +let HELLO: String = "HELLO" + +hello == HELLO // false +CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true +``` + +到目前为止,这个方法看起来和前文提到的方案,即创建一个包含 `String` 值的自定义 `CaseInsensitive` 类型,没什么区别。不过想要让自定义的 `CaseInsensitive` 类型变得和 `String` 一样好用,我们还需要考虑实现诸如 `ExpressibleByStringLiteral` 在内的其他协议,所以这才是漫漫长路的开始。 + +不过属性修饰器允许我们抛开这些繁琐的工作: + +```swift +struct Account: Equatable { + @CaseInsensitive var name: String + + init(name: String) { + $name = CaseInsensitive(wrappedValue: name) + } +} + +var johnny = Account(name: "johnny") +let JOHNNY = Account(name: "JOHNNY") +let Jane = Account(name: "Jane") + +johnny == JOHNNY // true +johnny == Jane // false + +johnny.name == JOHNNY.name // false + +johnny.name = "Johnny" +johnny.name // "Johnny" +``` + +这里,`Account` 对象通过 `name` 属性进行了一次判等,且判等的过程中不区分字母的大小写。可是当我们去获取或设置 `name` 属性时,它又像一个 *真正的* `String` 值一样区分字母大小写了。 + +*这很整洁,但这里到底发生了什么?* + +自 Swift 4 以后,如果某个类型里的存储属性都遵守了 `Equatable ` 协议的话,那么编译器将自动为这个类型增加 `Equatable` 的能力。因为这些实现是隐式的(至少目前看起来是这样),属性修饰器是通过被封装的值进行判等的,而不是对构成属性修饰器的值判等。 + +```swift +// 由 Swift 编译器生成 +extension Account: Equatable { + static func == (lhs: Account, rhs: Account) -> Bool { + lhs.$name == rhs.$name + } +} +``` + +#### 举一反三 + +- 定义 `@CompatibilityEquivalence` 属性修饰器,当修饰 `String` 类型的属性时,带有 `"①"` 和 `"1"` 时会被认为相等。 +- 实现一个 `@Approximate` 属性修饰器,来重新定义浮点数类型的相等语义 (另见 [SE-0259](https://github.com/apple/swift-evolution/blob/master/proposals/0259-approximately-equal.md))。 +- 实现一个 `@Ranked` 属性修饰器,它会带有一个函数,函数中定义了枚举值的排序;而这个排序需要符合我们通常打牌时的规则,例如牌面为 A 时,它既有可能是最大值,也可能是最小值。 + + +## 审查属性访问 + +业务要求可能会用某些控制措施,规定谁可以访问哪些记录,或者规定一些形式表格要随着时间变换。 + +重申一下,类似这样的功能通常不会在 iOS 端上完成;大多数业务逻辑都是在服务端完成的,许多客户端开发者并不想与这样的业务逻辑打交道。而下面的这个例子打开了一个新的视角来看待这个问题,当然这也归功于属性修饰器的功劳。 + +### 为属性值增加版本记录 + +下面的 `Versioned` 结构体函数用作一个属性修饰器,拦截了输入的值,并在设置每个值的时候创建带时间戳的记录。 + +```swift +import Foundation + +@propertyWrapper +struct Versioned { + private var value: Value + private(set) var timestampedValues: [(Date, Value)] = [] + + var wrappedValue: Value { + get { value } + + set { + defer { timestampedValues.append((Date(), value)) } + value = newValue + } + } + + init(initialValue value: Value) { + self.wrappedValue = value + } +} +``` + +下面是 `ExpenseReport` 类,它带有一个名为 `state` 的属性并被 `@Versioned` 属性修饰期所修饰。通过这种方式,我们可以回溯每一次的操作记录。 + +```swift +class ExpenseReport { + enum State { case submitted, received, approved, denied } + + @Versioned var state: State = .submitted +} +``` + +### 举一反三 + +- 实现一个 `@Audited` 属性修饰器,在每次读写属性的时候打印日志。 +- 实现一个 `@Decaying` 属性修饰器,它在每次值被读取的时候都会去除以一个设定的值。 + +--- + +不可否认的是,这个特定的示例还是暴露了属性修饰器的一些局限性:**属性无法被标记为 `throws`。**当然这个问题的根源还是在 Swift 语言自身上。 + +由于在错误处理上的能力欠缺,属性修饰器并没有什么好办法让代码完全按照你的设想执行。例如我们想让 `@Versioned` 属性修饰器支持这样一个特性,即在设置 `state` 属性时 ,当属性被设置为 `.denied` 后,就不能再被设置为 `.approved`,针对这种场景,现有的最佳方案是 `fatalError()`,但在实际的生产环境中,这可就不一定了: + +```swift +class ExpenseReport { + @Versioned var state: State = .submitted { + willSet { + if newValue == .approved, + $state.timestampedValues.map { $0.1 }.contains(.denied) + { + fatalError("J'Accuse!") + } + } + } +} + +var tripExpenses = ExpenseReport() +tripExpenses.state = .denied +tripExpenses.state = .approved // Fatal error: "J'Accuse!" +``` + +属性修饰器的局限性还有不少,这里提到的只是其中一点。所以为了更理性的看待这个特性,文章剩下的篇幅将会说说它的局限性都体现在哪里。 + +## 局限性 + +> 受我目前的理解能力和想象能力所限,下面给出的观点可能比较主观,有可能并不是属性修饰器这个提议本身造成的。 +> 如果你有任何好的建议或者意见,欢迎 [联系我们](https://twitter.com/NSHipster/) 。 + +### 属性不能参与错误处理 + +属性不像函数,无法使用 `throws` 标记。 + +关于上面提到的问题,原本就是函数与属性之间为数不多的区别之一。由于属性同时拥有获取方法(getter)和设置方法(setter),所以在这里如何进行错误处理并没有明确的最佳实践。尤其是你需要在兼顾访问控制,自定义获取方法/设置方法和回调的状态下,还写出优雅的语句。 + +如上一节所示,可以通过下面两种方式来处理非法值问题: + +1. 忽略它们(静默地) +2. 用 `fatalError()` 抛出崩溃。 + +不论哪一种方案都不够优雅,所以如果你对这个问题有更好的解决方案,欢迎分享。 + +### 属性修饰器无法起别名 + +这个提议的另外一个限制就是,你不能使用属性修饰器的实例作为属性修饰器。 + +还记得前面提到的 `UnitInterval` 么?我们可以用它来限制属性值的范围在 0 到 1 之间。所以我们是不是可以用写成下面的样子呢?: + +```swift +typealias UnitInterval = Clamping(0...1) // ❌ +``` + +可惜这样是不被允许的。同样你也不能使用属性修饰器的实例来修饰属性。 + +```swift +let UnitInterval = Clamping(0...1) +struct Solution { @UnitInterval var pH: Double } // ❌ +``` + +上面的代码说明一个问题,在实际使用过程中,我们可能会写出比预期多的重复代码。但考虑到这个问题的本质是计算机编程语言中值与类型是两种完全不同的东西引起的。所以从避免错误抽象的角度来看,这一小点的重复是完全可以忍受的。 + +### 属性修饰器很难组合 + +属性修饰器的组合不是一个可交换的操作;你声明它们的顺序影响了它们的作用顺序。 + +属性在进行 [字符串字符串的 string inflection 操作](https://nshipster.com/valuetransformer/#thinking-forwards-and-backwards) 和 string transforms 操作会互相影响。例如下面的属性修饰器组合,它的功能是将博客文章中的 URL “slug” 属性自动格式化,但这里的问题在于将短划线替换成空格的操作和去除空格的操作会互相影响,进而导致最终的结果发生变化。 + +```swift +struct Post { + <#...#> + @Dasherized @Trimmed var slug: String +} +``` + +但是,要让它先发挥作用,说起来容易做起来难!尝试组合 `String` 值的两个属性修饰器方法失败,因为最外层修饰器影响了在最内层的修饰器类型的值。 + +```swift +@propertyWrapper +struct Dasherized { + private(set) var value: String = "" + + var wrappedValue: String { + get { value } + set { value = newValue.replacingOccurrences(of: " ", with: "-") } + } + + init(initialValue: String) { + self.wrappedValue = initialValue + } +} + +struct Post { + <#...#> + @Dasherized @Trimmed var slug: String // ⚠️ 发生内部错误. +} +``` + +目前是有一个办法实现这个特性,但并不怎么优雅。关于这个问题是会在后续的版本中进行修复,还是通过文档正式说明都需要我们耐心的等待。 + +### 属性修饰器不是一等依赖类型 + +*依赖类型* 是由它的值定义的类型。例如,“一对后者比前者更大的整数”和“一个具有素数元素的数组”都是依赖类型,因为他们的类型定义取决与他们的值。 + +在 Swift 的类型系统里缺少对依赖类型的支持,如果想获得相关的特性需要在运行时完成。 + +好消息是,相比于其他语言,Swift 的属性修饰器算是第一个吃螃蟹的,不过即使这样,属性修饰器还不能算是一个完整的值依赖类型解决方案。 + +例如,你还是不能使用属性修饰器定义一个新类型,即使属性修饰器本身没什么毛病。 + +```swift +typealias pH = @Clamping(0...14) Double // ❌ +func acidity(of: Chemical) -> pH {} +``` + +你也不能使用属性修饰器去注解集合中的键类型或值类型。 + +```swift +enum HTTP { + struct Request { + var headers: [@CaseInsensitive String: String] // ❌ + } +} +``` + +这些缺点还是可以忍受的。属性修饰器非常有用,并且弥补了语言中的重要空白。 + +不知道属性修饰器的诞生会不会重燃大家对依赖类型的关注,当然另外一种可能是大家觉得当前的状态“也不是不能用”,也就没必要将依赖类型这个概念进一步正式化。 + +### 属性修饰器难以被文档化 + +**突击测验:**SwiftUI 框架提供了哪些可用的属性修饰器? + +去吧,看下 [SwiftUI 官方文档](https://developer.apple.com/documentation/swiftui),然后试着回答。 + +😬 + +公平地讲,这种失败不是属性修饰器所特有的。 + +如果你的任务是明确标准库中某个 API 都需要哪些协议响应,或是在 `developer.apple.com` 文档中明确某个运算符都支持哪些类型时,你其实就可以考虑转行了。 + +随着 Swift 的复杂性不断增加,它的可理解性就会不断下降,我想没有比这更让人头疼了吧。 + +### 属性修饰器让 Swift 进一步复杂化 + +Swift 是一门比 Objective-C *更加* 复杂的语言。自 Swift 1.0 以来,这就是一条不变的真理。 + +在 Swift 中有大量的 `@` 前缀,从 Swift 4 提出的 [`@dynamicMemberLookup`](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md) 和 [`@dynamicCallable`](https://github.com/apple/swift-evolution/blob/master/proposals/0216-dynamic-callable.md) ,到 [Swift for Tensorflow](https://github.com/tensorflow/swift) 里的 [`@differentiable` 和 `@memberwise`](https://forums.swift.org/t/pre-pitch-swift-differentiable-programming-design-overview/25992),即使有文档在手,这些东西也使得 Swift 的 API 越来越难理解。从这个角度来看,`@propertyWrapper` 无疑是加重了这个问题的严重性。 + +我们要如何理解这一切?(这是一个客观的真是问题,不是反问。) + +--- + +好了,现在让我们总结一下这个新特性—— + +属性修饰器能够让开发者使用到更高层级的语言特性,而这在以前是不可能的。这个提议在提高代码安全性和降低代码复杂性上有巨大的潜力,现阶段我们只是看到了它的一些基本可能性而已。 + +然而,他们有所承诺,属性修饰器及其他语言特性与 SwiftUI 一起的首次亮相将给 Swift 带来了巨大的变化。果不其然,如他们之前承诺的一样,属性修饰器和其他的新特性随着 SwitUI 在这个夏天闪亮登场,而这一次亮相,为整个 Swift 生态环境带来了巨大的变化。 + +或者,正如 Nataliya Patsovska 在 [一篇推特](https://twitter.com/nataliya_bg/status/1140519869361926144) 中所提到的: + +> iOS API 设计简史: +> +> - Objective C - 在名字中描述了所有语义,类型并不重要 +> - Swift 1 到 5 - 名字侧重于清晰度,基础结构体,枚举,类和协议持有语义 +> - Swift 5.1 - @wrapped \$path @yolo +> +> ——[@nataliya_bg](https://twitter.com/nataliya_bg/) + +也许我们后面回头看才能知道, Swift 5.1 是不是为我们热爱的语言树立了一个临界点或者转折点。 \ No newline at end of file From ef5d0202f571849597fbe539305d5e382148b0f3 Mon Sep 17 00:00:00 2001 From: Nemocdz Date: Sat, 30 May 2020 23:34:34 +0800 Subject: [PATCH 14/20] fix typo --- khanlou/20180907_hacking-hit-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khanlou/20180907_hacking-hit-tests.md b/khanlou/20180907_hacking-hit-tests.md index 1addcbe2..58ea363c 100644 --- a/khanlou/20180907_hacking-hit-tests.md +++ b/khanlou/20180907_hacking-hit-tests.md @@ -21,7 +21,7 @@ description: -`UIKit` 有个十分古怪的地方,那是它的触摸事件处理系统。它主要包括两个方法,`-pointInstide:withEvent:` 和 `-hitTest:withEvent:`。 +`UIKit` 有个十分古怪的地方,那是它的触摸事件处理系统。它主要包括两个方法,`-pointInside:withEvent:` 和 `-hitTest:withEvent:`。 `-pointInside:` 会告诉调用者给定点是否包含在指定的视图区域中。而 `-hitTest:` 用 `pointInside:` 这个方法来告诉调用者哪个子视图(如果有的话)是当前触摸在给定点的接收者。现在我比较感兴趣的是后面这个方法。 From de433e4a4ef2b6fdaff49bbd9083aa0ede2db655 Mon Sep 17 00:00:00 2001 From: Joeytat Date: Fri, 20 Dec 2019 00:30:29 +0800 Subject: [PATCH 15/20] add swift-protocols-app-configuration --- ...91220_swift-protocols-app-configuration.md | 608 ++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 appcoda/20191220_swift-protocols-app-configuration.md diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md new file mode 100644 index 00000000..fc68c577 --- /dev/null +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -0,0 +1,608 @@ +title: "如何利用 Swift 协议来管理应用配置" +date: 2019-12-20 +tags: [教程] +categories: [AppCoda] +permalink: swift-protocols-app-configuration +keywords: 协议,应用配置 +custom_title: 如何利用 Swift 协议来管理应用配置 +description: 本文详细讲解了如何利用 Swift 协议来管理应用配置。 + +--- +原文链接=https://www.appcoda.com/swift-protocols-app-configuration/ +作者=Gabriel Theodoropoulos +原文日期=2019-08-30 +译者=Joeytat +校对= +定稿= + + +大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我不认为有任何一个程序员不知道协议。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: + +> 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是满足了该协议。 + + +用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 + +让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为一个 Swift 开发中的流行话题的原因。通过定义一组方法来描述一系列功能,并对他们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 + +如果你是一个新手开发者,那么我强烈推荐你阅读[**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 + +好了,那到底协议与这篇文章标题所说的应用的设置有什么关系吗?让我来解释一下,并且将其建立起联系。很久以来,我一直都被 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用他们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! + +# 线路图 +我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。我们将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,再到最后的一个指令,就会让它完全正常运转。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来我们将要创建的文件是 *属性列表*(.plist)。毕竟当我们考虑设置时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们让要加上对应用包中的默认初始设置的处理支持(那些我们可以在 Xcode 属性列表编辑器中编辑的设置)。 + +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里我们是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 + +最后,这里有一个供你下载的[启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用来放在协议中改变。在你下载之后,请用 Xcode 打开然后继续往下。 + +# 开始:创建协议 +让我们打开启动项目,然后创建一个将用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 + +下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 Create 按钮,来让 Xcode 真正地创建新文件。 + +> 注意: 这篇文章中的代码是在 Xcode 10.3 中创建的,这是在撰写这篇文章时最新且最稳定的 Xcode 版本。 + +当新文件准备好时,去 `Project Navigator` 选中并且打开它。协议的定义是: + +```swift +protocol SettingsManageable { + +} +``` + +我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样我们可以通过采纳协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: + +```swift +extension SettingsManageable where Self: Codable { + +} +``` + +注意我们在这里设置的条件。通过扩展尾部的 `where Self: Codable` 条件,我们要求了任何自定义类型在采纳 `SettingsManageable` 协议的同时,也要采纳 [`Codable`](https://developer.apple.com/documentation/swift/codable) 协议。 + +为什么我们需要 `Codable`?因为我们会对采纳了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 + +以防止有的类型并未采纳 `Codable` 但采纳了 `SettingsManageable`,这样就别指望我们接下来能够为其提供正确的功能了。 + +# 定义并且实现协议的要求 +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何采纳了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! + +## 获取文件 URL +任何采纳了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! + +向协议中添加下面方法的定义: + +```swift +protocol SettingsManageable { + func settingsURL() -> URL +} +``` + +在协议扩展中我们对其实现: + +```swift +extension SettingsManageable where Self: Codable { + func settingsURL() -> URL { + let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + return cachesDirectory.appendingPathComponent("\(Self.self).plist") + } +} +``` + +第一行我们从缓存目录获取到了 URL。第二行我们将*采纳了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 + +注意 `\(Self.self)` 动态地提供了采纳 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类采纳了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体采纳了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 + +## 存储(更新)一个 SettingsManageable 实例 +毋庸置疑我们上面实现的方法是非常实用的,但是它也只是一个为了我们方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将采纳协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: + + 1. 编码为属性列表 + 2. 写入文件 + +再次回到 `SettingsManageable` 协议中,添加下面的定义: + +```swift +protocol SettingsManageable { + ... + func update() -> Bool +} +``` + +我们将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 + +我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 对象转化成 JSON。有很大概率你已经用过了它!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? + +噢,是的,就是有这么一个类,而且我们将要用上它!在协议扩展中,先写上如下代码: + +```swift +func update() -> Bool { + do { + let encoded = try PropertyListEncoder().encode(self) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先我们创建了一个 `PropertyListEncoder` 的类,然后我们调用了 `encode(_:)` 方法来对 `self`(采纳了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! + +如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里我们并未对异常做任何额外操作,我们只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 + +上面的代码并没有将编码后的数据*写入*文件。在 `do` 中,再加上这么一行: + +```swift +do { + let encoded = try PropertyListEncoder().encode(self) + try encoded.write(to: settingsURL()) + return true +} +``` + +`Data` 类中的 `write(to:)` 方法同样会抛出异常, 所以配合 `try` 标记使用也是强制要求的。注意在这里我们就用上了之前实现的获取文件 URL 的 `settingsURL()` 方法。 + +只通过两行重要的代码,我们就实现了编码和将目标类型写入属性列表文件的功能。整个方法应该像这样: + +```swift +func update() -> Bool { + do { + let encoded = try PropertyListEncoder().encode(self) + try encoded.write(to: settingsURL()) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望我们的方法内部使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: + +```swift +func update() throws +``` + +然后在协议扩展中这样修改: + +```swift +func update() throws { + let encoded = try PropertyListEncoder().encode(self) + try encoded.write(to: settingsURL()) +} +``` + +可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了方便你,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 + +> 注意:想要了解更多关于错误处理的内容?看看[这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)吧! + +## 通过文件加载设置 +现在让我们过渡到通过文件加载数据,我们刚刚提到的保存数据的对应部分。我们要跟随的步骤很简单,所有要做的就以下几点: + +1. 检查文件是否存在。 +2. 将文件内容加载为 `Data` 对象。 +3. 通过 **`PropertyListDecoder`** 类来对其解码,与 `PropertyListEncoder` 相对应。 + +和我们之前做的相似,这个方法也会返回一个布尔值来表示成功与否。在 `SettingsManageable` 协议中添加下面的内容: + +```swift +protocol SettingsManageable { + ... + mutating func load() -> Bool +} +``` + +这个方法被标记为*可变*,因为它会修改采纳了 `SettingsManageable` 协议的类型的实例(简单来说,它会修改 `self`)。 + +这个方法的实现其实很简单并且和我们之前做的很相似。只是额外增加了对文件是否存在的检查。在协议扩展的方法中添加如下实现: + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + do { + let fileContents = try Data(contentsOf: settingsURL()) + self = try PropertyListDecoder().decode(Self.self, from: fileContents) + return true + } catch { + print(error.localizedDescription) + return false + } + } +} +``` + +我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次我们需要使用 `path` 属性来获取路径字符串。当我们确定文件存在之后,我们就从文件中加载内容到 `fileContents` 实例中,然后我们就拿到加载的数据,通过 `PropertyListDecoder` 类解码后,对 `self`(采纳了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,我们就返回 true,如果出现了错误则会抛出异常,然后我们会将错误打印在控制台并且返回 false。 + +现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况我们还没有返回值。如果没有文件可以加载设置,那么我们就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + ... + } else { + return update() + } +} +``` + +现在方法在所有的情况下都有返回值了。如果我们第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 + +## 通过应用包中的 Plist 文件初始化设置 +既然我们一直在聊设置,那么很自然就会想到他们的的默认值。在编程层面,采纳了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者唯一适合的方式。通过 Xcode 的属性列表编辑器来编辑放在应用包中的属性列表文件,以初始化属性列表文件或许是更好也跟简单的方式。我们将要在这个部分覆盖这种情况,然后还有**一条很重要的规则**需要继续: + +*属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* + +否则的话,解码会失败。 + +让我们通过在 `SettingsManageable` 协议中定义一个新的方法开始吧: + +```swift +protocol SettingsManageable { + ... + + mutating func loadUsingSettingsFile() -> Bool +} +``` + +在这个方法的实现中(协议扩展里),我们要做的第一件事就是检查应用包中是否存在一个初始设置文件: + +```swift +mutating func loadUsingSettingsFile() -> Bool { + guard let originalSettingsURL = Bundle.main.url(forResource: "\(Self.self)", withExtension: "plist") + else { return false } +} +``` + +**记住**:设置文件的名字应该和采纳了 `SettingsManageable` 的类或结构体名字相同。 + +下一步,我们将会检查缓存目录中是否已经存在属性列表文件。如果*不存在*,那么我们将*应用包中的文件拷贝到缓存目录中*: + +```swift +do { + if !FileManager.default.fileExists(atPath: settingsURL().path) { + try FileManager.default.copyItem(at: originalSettingsURL, to: settingsURL()) + } + +} catch { + print(error.localizedDescription) + return false +} +``` + +如果文件已经存在,条件判断则会为 false,那么拷贝的操作将不会执行!现在,我们要做的和之前一摸一样;我们会加载文件内容到 `Data` 对象中,然后我们会将其解码: + +```swift +do { + ... + + let fileContents = try Data(contentsOf: settingsURL()) + self = try PropertyListDecoder().decode(Self.self, from: fileContents) + return true + +} catch { ... } +``` + +无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常工作。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 + +## 删除设置文件 +对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: + +```swift +protocol SettingsManageable { + ... + + func delete() -> Bool +} +``` + +毫无保留地在协议扩展中提供方法的实现: + +```swift +func delete() -> Bool { + do { + try FileManager.default.removeItem(at: settingsURL()) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +如果移除文件像上面这样成功了,那么方法会返回 true。如果在移除过程中产生了任何错误,那么返回 false。 + +## 重置设置 +如果有办法可以在任意时间切换回最初的设置,那将会是个很有用的功能。这背后的含义其实是删除缓存目录中的属性列表文件,然后再把初始值写回去。 + +当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,当初始值被赋给了属性,事情就变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,又重新保存到文件中,这显然是错误的! + +为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得于第一份创建在缓存目录的文件。我们将会以它来重置设置为初始值,并且这样的话,我们也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 + +为了我刚刚提到的这个目的,我们还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以我们不会将它们定义在协议中。取而代之,我们会将其在协议扩展中实现的地方,并将它们标记为 `private`。第一个方法是将原始文件备份: + +```swift +private func backupSettingsFile() { + do { + try FileManager.default.copyItem(at: settingsURL(), to: settingsURL().appendingPathExtension("init")) + } catch { + print(error.localizedDescription) + } +} +``` + +拥有初始设置的文件将会以 “init” 作为文件扩展名。举个例子,原始设置文件叫 “AppSettings.plist”,那备份就会叫做 “AppSettings.plist.init”。 + +这个方法则是将初始设置拷贝到正常设置文件目录下: + +```swift +private func restoreSettingsFile() -> Bool { + do { + try FileManager.default.copyItem(at: settingsURL().appendingPathExtension("init"), to: settingsURL()) + return true + } catch { + print(error.localizedDescription) + return false + } +} +``` + +有了上面的两个方法,就让我们回到 `load()` 方法,在 `else` 条件下如果文件不存在,则会将属性列表文件第一次写入缓存目录。此刻这个地方看起来是这样: + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + ... + } else { + return update() + } +} +``` + +我们将会对其作出修改,我们会在 `update()` 方法之后调用 `backupSettingsFile()`,只要 `update()` 返回的是 true。这是修改之后的样子: + +```swift +mutating func load() -> Bool { + if FileManager.default.fileExists(atPath: settingsURL().path) { + ... + } else { + if update() { + backupSettingsFile() + return true + } else { return false } + } +} +``` + +现在我们可以专注到重置方法的实现啦。在 `SettingsManageable` 协议中添加: + +```swift +protocol SettingsManageable { + ... + + mutating func reset() -> Bool +} +``` + +在协议扩展中实现它。我们先从删除当前缓存目录中的属性列表文件开始: + +```swift +mutating func reset() -> Bool { + if delete() { + + } + + return false +} +``` + +下一步我们要小心一点。我们想要让 `reset()` 方法与加载设置的方法独立开来。因为这个原因,我们会首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后我们会调用 `load()` 方法来加载设置。 + +这就是上面所描述「场景」的代码: + +```swift +mutating func reset() -> Bool { + if delete() { + if !loadUsingSettingsFile() { + if restoreSettingsFile() { + return load() + } + } else { + return true + } + } + return false +} +``` + +如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! + +# 将属性列表内容当作字典 +上面我们实现的所有的 `SettingsManageable` 协议中的方法所瞄准的目标,都是为所有采纳了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像我们遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以既然这篇文章给了机会可以聊一下属性列表,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如果将属性列表作为字典类型获取。 + +在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: + +```swift +protocol SettingsManageable { + ... + + func toDictionary() -> [String: Any?]? +} +``` + +这个方法会返回一个范性为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件无法被解码的过程中发生了什么糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 + +在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个我们之前已经做过好几次了。同时,我们还会把文件内容加载到 `Data` 对象中: + +```swift +func toDictionary() -> [String: Any?]? { + do { + if FileManager.default.fileExists(atPath: settingsURL().path) { + let fileContents = try Data(contentsOf: settingsURL()) + } + } catch { + print(error.localizedDescription) + } + + return nil +} +``` + +可以看到如果文件不存在我们将直接返回 nil。接下来是重点,`PropertyListSerialization` 类提供了一个叫作 `propertyList(from:options:format:)` 的方法,可以从 `Data` 对象中返回一个属性列表对象(在我们这里就是 `fileContents` 对象)。我们这么使用这个方法: + +```swift +let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) +``` + +作为一个会抛出异常错误的方法,使用 `try` 关键字是必要的。它的结果是返回一个 `Any` 类型的对象,所以我们可以通过简单的转换来得到一个字典: + +```swift +let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) as? [String: Any?] +``` + +`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就很小的。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。然而既然我们正在讨论属性列表,这就是个很好的机会提及此方法。 + +现在我们要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? + +嘿,我将这个问题留给你作为可选的练习。给一个小提示,如果是我的话,会去找一个叫 `data(fromPropertyList:format:options)` 的方法。 + +# SettingsManageable 协议实战 +是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! + +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合[***单例***](https://en.wikipedia.org/wiki/Singleton_pattern)的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实,然后在需要的时候,将其当作其他普通的类来初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是属性的默认值中设置的。 + +我们需要做的第一件事就是让 `AppSettings` 类采纳 `SettingsManageable` 协议。不要忘了也要采纳 `Codable` 协议: + +```swift +class AppSettings: Codable, SettingsManageable { + ... +} +``` + +因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过我们之前实现的 `load()` 方法来加载设置: + +```swift +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + + _ = AppSettings.shared.load() + + return true +} +``` + +如果应用第一次启动,那么当 `load()` 方法调用时,初始设置会被写入 “AppSettings.plist” 文件,保存在缓存目录中。与此同时,这些初始设置也会被被分到 “AppSettings.plist.init” 文件中,这样我们可以在需要的时候通过它来重置设置。如果 .plist 文件已经在应用启动的时候存在了,那么设置会被加载并且赋到 `AppSettings` 共享实例的属性上。 + +现在再到 `ViewController.swift` 文件,移动到 `tryAppSettings()` 方法中。我们将会修改一些设置: + +```swift +func tryAppSettings() { + AppSettings.shared.fontSize = 21.0 + AppSettings.shared.playSFX = false +} +``` + +让我们来更新一下设置文件,然后看看我们写入了什么: + +```swift +if AppSettings.shared.update() { + if let dictionary = AppSettings.shared.toDictionary() { + print(dictionary.compactMapValues { $0 }) + } +} +``` + +上面的代码片段除了 `update()` 之外我们还使用了 `toDictionary()` 方法,并且我们还将文件内容加载进了字典对象。然后我们将字典中除 nil 之外的值打印了出来。 + +同时,为了验证文件是否被成功创建,再添加一行代码,它会打印应用的真实的缓存目录,你可以通过 Finder 来查看: + +```swift +print(AppSettings.shared.settingsURL().path) +``` + +如果你运行应用,你就能在控制台中看到如下输出: + +![console](https://www.appcoda.com/wp-content/uploads/2019/08/t67_3_results_1.png) + +如果你通过 Finder 跳转到打印出来的路径,你还能看到有两个文件被创建: + +![files_created](https://www.appcoda.com/wp-content/uploads/2019/08/t67_4_files1.png) + +让我们试试重置设置。在上面的指令后面再添加下面的代码: + +```swift +if AppSettings.shared.reset() { + if let dictionary = AppSettings.shared.toDictionary() { + print(dictionary.compactMapValues { $0 }) + } +} +``` + +再次运行。这次你会先看到被修改的值,然后是原始值。这意味着我们重置到初始设置如预期的成功了! + +![reset_settings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_5_results_2.png) + +让我们切换到 *PlayerSettings.swift* 文件然后找到用来表示虚拟游戏玩家设置的 `PlayerSettings` 结构体。首先让 `PlayerSettings` 结构体采纳 `Codable` 和 `SettingsManageable` 协议: + +```swift +struct PlayerSettings: Codable, SettingsManageable { + ... +} +``` + +与 `AppSettings` 类所使用的属性相反,这里初始值并没有直接赋给属性。与之替代的是存在于应用包中的 *PlayerSettings.plist* 文件,你可以在 Project Navigator 中找到。当你打开文件时,注意在 .plist 文件中的键名和属性名相同。 + +回到 `ViewController.swift` 文件,找到 `tryPlayerSettings()` 方法。要做的第一件事是初始化一个 `PlayerSettings` 对象,然后加载设置,但这一次我们将会使用 `loadUsingSettingsFile()` 方法。同样的,我们也会像之前那样将结果打印出来: + +```swift +func tryPlayerSettings() { + var playerSettings = PlayerSettings() + if playerSettings.loadUsingSettingsFile() { + if let dictionary = playerSettings.toDictionary() { + print(dictionary.compactMapValues { $0 }) + } + } + +} +``` + +下面是我们运行应用之后打印在控制台的结果: +![console2](https://www.appcoda.com/wp-content/uploads/2019/08/t67_6_results_3.png) + +以及这是与 *AppSettings.plist* 同时存在的 *PlayerSettings.plist* 文件: +![PlayerSettings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_7_files2.png) + +让我们来修改一些属性,然后更新 .plist 文件,并且再次打印: + +```swift +playerSettings.isMale = false +playerSettings.gunType = 1 +playerSettings.powerLevels?[0] = 1 +playerSettings.powerLevels?[1] = 1 +playerSettings.powerLevels?[3] = 5 +playerSettings.powerLevels?[4] = 5 +_ = playerSettings.update() +if let dictionary = playerSettings.toDictionary() { + print("\n", dictionary.compactMapValues { $0 }) +} +``` + +注意在真正的应用中,我们应该检查下标是否越界! + +这里是从 .plist 文件中获取到的更新后的设置: +![updated_plist](https://www.appcoda.com/wp-content/uploads/2019/08/t67_8_results_4.png) + +最后,让我们尝试着重置设置,然后验证一下原始的设置文件是否会覆盖当前在缓存目录中的设置文件: + +```swift +_ = playerSettings.reset() +if let dictionary = playerSettings.toDictionary() { + print("\n", dictionary.compactMapValues { $0 }) +} +``` +![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) + +# 总结 +我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能性以及对于所有应用来说都很必要的自动化任务。同时,利用了我们这里提到的解决方案,增加了工作的效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! + +作为参考,你可以在 GitHub 上下载到[完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From 0e56c962c9d24e792a24fe43f236147cc16632b7 Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Mon, 30 Dec 2019 13:04:39 +0800 Subject: [PATCH 16/20] update swift-protocols-app-configuration --- ...91220_swift-protocols-app-configuration.md | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index fc68c577..e5e8f0d4 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -23,18 +23,18 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 -让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为一个 Swift 开发中的流行话题的原因。通过定义一组方法来描述一系列功能,并对他们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 +让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 -如果你是一个新手开发者,那么我强烈推荐你阅读[**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 +如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 -好了,那到底协议与这篇文章标题所说的应用的设置有什么关系吗?让我来解释一下,并且将其建立起联系。很久以来,我一直都被 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用他们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! +好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! # 线路图 -我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。我们将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,再到最后的一个指令,就会让它完全正常运转。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来我们将要创建的文件是 *属性列表*(.plist)。毕竟当我们考虑设置时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们让要加上对应用包中的默认初始设置的处理支持(那些我们可以在 Xcode 属性列表编辑器中编辑的设置)。 +我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 -在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里我们是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 -最后,这里有一个供你下载的[启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用来放在协议中改变。在你下载之后,请用 Xcode 打开然后继续往下。 +最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议进行改造。在你下载之后,请用 Xcode 打开然后往下阅读。 # 开始:创建协议 让我们打开启动项目,然后创建一个将用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 @@ -51,7 +51,7 @@ protocol SettingsManageable { } ``` -我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样我们可以通过采纳协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: +我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: ```swift extension SettingsManageable where Self: Codable { @@ -59,17 +59,17 @@ extension SettingsManageable where Self: Codable { } ``` -注意我们在这里设置的条件。通过扩展尾部的 `where Self: Codable` 条件,我们要求了任何自定义类型在采纳 `SettingsManageable` 协议的同时,也要采纳 [`Codable`](https://developer.apple.com/documentation/swift/codable) 协议。 +注意在这里设置的条件。通过扩展尾部的 `where Self: Codable` 条件,我们要求了任何自定义类型在遵循 `SettingsManageable` 协议的同时,也要遵循 [`Codable`](https://developer.apple.com/documentation/swift/codable) 协议。 -为什么我们需要 `Codable`?因为我们会对采纳了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 +为什么我们需要 `Codable`?因为我们会对遵循了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 -以防止有的类型并未采纳 `Codable` 但采纳了 `SettingsManageable`,这样就别指望我们接下来能够为其提供正确的功能了。 +以免有的类型并未遵循 `Codable` 但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 # 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何采纳了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! ## 获取文件 URL -任何采纳了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! +任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! 向协议中添加下面方法的定义: @@ -90,12 +90,12 @@ extension SettingsManageable where Self: Codable { } ``` -第一行我们从缓存目录获取到了 URL。第二行我们将*采纳了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 +第一行从缓存目录获取到了 URL。第二行将*遵循了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 -注意 `\(Self.self)` 动态地提供了采纳 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类采纳了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体采纳了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 +注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 ## 存储(更新)一个 SettingsManageable 实例 -毋庸置疑我们上面实现的方法是非常实用的,但是它也只是一个为了我们方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将采纳协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: +毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 2. 写入文件 @@ -109,7 +109,7 @@ protocol SettingsManageable { } ``` -我们将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 +将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 对象转化成 JSON。有很大概率你已经用过了它!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? @@ -127,7 +127,7 @@ func update() -> Bool { } ``` -如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先我们创建了一个 `PropertyListEncoder` 的类,然后我们调用了 `encode(_:)` 方法来对 `self`(采纳了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! +如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先创建了一个 `PropertyListEncoder` 的类,然后调用了 `encode(_:)` 方法来对 `self`(遵循了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! 如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里我们并未对异常做任何额外操作,我们只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 @@ -158,7 +158,7 @@ func update() -> Bool { } ``` -在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望我们的方法内部使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: +在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望方法内部所使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: ```swift func update() throws @@ -173,12 +173,12 @@ func update() throws { } ``` -可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了方便你,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 +可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了让你方便,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 -> 注意:想要了解更多关于错误处理的内容?看看[这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)吧! +> 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html) 吧! ## 通过文件加载设置 -现在让我们过渡到通过文件加载数据,我们刚刚提到的保存数据的对应部分。我们要跟随的步骤很简单,所有要做的就以下几点: +现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。我们要跟随的步骤很简单,只需要跟着下面这几步: 1. 检查文件是否存在。 2. 将文件内容加载为 `Data` 对象。 @@ -193,7 +193,7 @@ protocol SettingsManageable { } ``` -这个方法被标记为*可变*,因为它会修改采纳了 `SettingsManageable` 协议的类型的实例(简单来说,它会修改 `self`)。 +这个方法被标记为*可变*,因为它会修改遵循了 `SettingsManageable` 协议的类型的实例(简单来说,它会修改 `self`)。 这个方法的实现其实很简单并且和我们之前做的很相似。只是额外增加了对文件是否存在的检查。在协议扩展的方法中添加如下实现: @@ -212,9 +212,9 @@ mutating func load() -> Bool { } ``` -我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次我们需要使用 `path` 属性来获取路径字符串。当我们确定文件存在之后,我们就从文件中加载内容到 `fileContents` 实例中,然后我们就拿到加载的数据,通过 `PropertyListDecoder` 类解码后,对 `self`(采纳了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,我们就返回 true,如果出现了错误则会抛出异常,然后我们会将错误打印在控制台并且返回 false。 +我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次需要使用 `path` 属性来获取路径字符串。当确定文件存在之后,就从文件中加载内容到 `fileContents` 实例中,拿到加载的数据后,通过 `PropertyListDecoder` 类解码后,对 `self`(遵循了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,返回 true,如果出现了错误则会抛出异常,然后将错误打印在控制台并且返回 false。 -现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况我们还没有返回值。如果没有文件可以加载设置,那么我们就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! +现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况还没有返回值。如果没有文件可以加载设置,那么就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! ```swift mutating func load() -> Bool { @@ -226,16 +226,16 @@ mutating func load() -> Bool { } ``` -现在方法在所有的情况下都有返回值了。如果我们第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 +现在方法在所有的情况下都有返回值了。如果是第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 ## 通过应用包中的 Plist 文件初始化设置 -既然我们一直在聊设置,那么很自然就会想到他们的的默认值。在编程层面,采纳了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者唯一适合的方式。通过 Xcode 的属性列表编辑器来编辑放在应用包中的属性列表文件,以初始化属性列表文件或许是更好也跟简单的方式。我们将要在这个部分覆盖这种情况,然后还有**一条很重要的规则**需要继续: +既然我们一直在聊设置,那么很自然就会想到它们的的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: *属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* 否则的话,解码会失败。 -让我们通过在 `SettingsManageable` 协议中定义一个新的方法开始吧: +让我们从 `SettingsManageable` 协议中定义一个新的方法开始吧: ```swift protocol SettingsManageable { @@ -245,7 +245,7 @@ protocol SettingsManageable { } ``` -在这个方法的实现中(协议扩展里),我们要做的第一件事就是检查应用包中是否存在一个初始设置文件: +在这个方法的实现中(协议扩展里),要做的第一件事就是检查应用包中是否存在一个初始设置文件: ```swift mutating func loadUsingSettingsFile() -> Bool { @@ -254,9 +254,9 @@ mutating func loadUsingSettingsFile() -> Bool { } ``` -**记住**:设置文件的名字应该和采纳了 `SettingsManageable` 的类或结构体名字相同。 +**记住**:设置文件的名字应该和遵循了 `SettingsManageable` 的类或结构体名字相同。 -下一步,我们将会检查缓存目录中是否已经存在属性列表文件。如果*不存在*,那么我们将*应用包中的文件拷贝到缓存目录中*: +下一步,我们将会检查缓存目录中是否已经存在属性列表文件。如果*不存在*,那么就将*应用包中的文件拷贝到缓存目录中*: ```swift do { @@ -270,7 +270,7 @@ do { } ``` -如果文件已经存在,条件判断则会为 false,那么拷贝的操作将不会执行!现在,我们要做的和之前一摸一样;我们会加载文件内容到 `Data` 对象中,然后我们会将其解码: +如果文件已经存在,条件判断则会为 false,那么拷贝的操作将不会执行!现在,要做的和之前一模一样;我们会加载文件内容到 `Data` 对象中,然后将其解码: ```swift do { @@ -296,7 +296,7 @@ protocol SettingsManageable { } ``` -毫无保留地在协议扩展中提供方法的实现: +协议扩展里的方法实现不是什么秘密: ```swift func delete() -> Bool { @@ -317,9 +317,9 @@ func delete() -> Bool { 当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,当初始值被赋给了属性,事情就变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,又重新保存到文件中,这显然是错误的! -为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得于第一份创建在缓存目录的文件。我们将会以它来重置设置为初始值,并且这样的话,我们也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 +为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得是第一次在缓存目录创建的文件。我们将会以它来重置设置为初始值,并且这样的话,也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 -为了我刚刚提到的这个目的,我们还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以我们不会将它们定义在协议中。取而代之,我们会将其在协议扩展中实现的地方,并将它们标记为 `private`。第一个方法是将原始文件备份: +为了达到这个目的,还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以不会将它们定义在协议中。取而代之的是在协议扩展中方法实现的地方将它们标记为 `private`。第一个方法是将原始文件备份: ```swift private func backupSettingsFile() { @@ -347,7 +347,7 @@ private func restoreSettingsFile() -> Bool { } ``` -有了上面的两个方法,就让我们回到 `load()` 方法,在 `else` 条件下如果文件不存在,则会将属性列表文件第一次写入缓存目录。此刻这个地方看起来是这样: +有了上面的两个方法,让我们回到 `load()` 方法,在 `else` 条件下如果文件不存在,则会将属性列表文件第一次写入缓存目录。此刻这个地方看起来是这样: ```swift mutating func load() -> Bool { @@ -396,7 +396,7 @@ mutating func reset() -> Bool { } ``` -下一步我们要小心一点。我们想要让 `reset()` 方法与加载设置的方法独立开来。因为这个原因,我们会首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后我们会调用 `load()` 方法来加载设置。 +下一步要小心一点。为了让 `reset()` 方法与加载设置的方法独立开来。因此,我们要首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后调用 `load()` 方法来加载设置。 这就是上面所描述「场景」的代码: @@ -418,7 +418,7 @@ mutating func reset() -> Bool { 如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! # 将属性列表内容当作字典 -上面我们实现的所有的 `SettingsManageable` 协议中的方法所瞄准的目标,都是为所有采纳了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像我们遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以既然这篇文章给了机会可以聊一下属性列表,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如果将属性列表作为字典类型获取。 +所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: @@ -430,9 +430,9 @@ protocol SettingsManageable { } ``` -这个方法会返回一个范性为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件无法被解码的过程中发生了什么糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 +这个方法会返回一个范型为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件在被解码的过程中,发生了如无法解码这样糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 -在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个我们之前已经做过好几次了。同时,我们还会把文件内容加载到 `Data` 对象中: +在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个之前已经做过好几次了。同时还要把文件内容加载到 `Data` 对象中: ```swift func toDictionary() -> [String: Any?]? { @@ -448,30 +448,30 @@ func toDictionary() -> [String: Any?]? { } ``` -可以看到如果文件不存在我们将直接返回 nil。接下来是重点,`PropertyListSerialization` 类提供了一个叫作 `propertyList(from:options:format:)` 的方法,可以从 `Data` 对象中返回一个属性列表对象(在我们这里就是 `fileContents` 对象)。我们这么使用这个方法: +可以看到如果文件不存在将直接返回 nil。接下来是重点,`PropertyListSerialization` 类提供了一个叫作 `propertyList(from:options:format:)` 的方法,可以从 `Data` 对象中返回一个属性列表对象(在我们这里就是 `fileContents` 对象)。方法这么使用: ```swift let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) ``` -作为一个会抛出异常错误的方法,使用 `try` 关键字是必要的。它的结果是返回一个 `Any` 类型的对象,所以我们可以通过简单的转换来得到一个字典: +作为一个会抛出异常错误的方法,使用 `try` 关键字是必要的。它的结果是返回一个 `Any` 类型的对象,所以可以通过简单的转换来得到一个字典: ```swift let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) as? [String: Any?] ``` -`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就很小的。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。然而既然我们正在讨论属性列表,这就是个很好的机会提及此方法。 +`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。自从有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就变小了。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。既然我们正在讨论属性列表,就可以顺便提及此方法。 -现在我们要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? +现在要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? 嘿,我将这个问题留给你作为可选的练习。给一个小提示,如果是我的话,会去找一个叫 `data(fromPropertyList:format:options)` 的方法。 # SettingsManageable 协议实战 是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! -首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合[***单例***](https://en.wikipedia.org/wiki/Singleton_pattern)的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实,然后在需要的时候,将其当作其他普通的类来初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是属性的默认值中设置的。 +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 -我们需要做的第一件事就是让 `AppSettings` 类采纳 `SettingsManageable` 协议。不要忘了也要采纳 `Codable` 协议: +我们需要做的第一件事就是让 `AppSettings` 类遵循 `SettingsManageable` 协议。不要忘了也要遵循 `Codable` 协议: ```swift class AppSettings: Codable, SettingsManageable { @@ -479,7 +479,7 @@ class AppSettings: Codable, SettingsManageable { } ``` -因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过我们之前实现的 `load()` 方法来加载设置: +因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过之前实现的 `load()` 方法来加载设置: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -491,7 +491,7 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau } ``` -如果应用第一次启动,那么当 `load()` 方法调用时,初始设置会被写入 “AppSettings.plist” 文件,保存在缓存目录中。与此同时,这些初始设置也会被被分到 “AppSettings.plist.init” 文件中,这样我们可以在需要的时候通过它来重置设置。如果 .plist 文件已经在应用启动的时候存在了,那么设置会被加载并且赋到 `AppSettings` 共享实例的属性上。 +如果应用第一次启动,那么当 `load()` 方法调用时,初始设置会被写入 “AppSettings.plist” 文件,保存在缓存目录中。与此同时,这些初始设置也会被备份到 “AppSettings.plist.init” 文件中,这样可以在需要的时候通过它来重置设置。如果 .plist 文件已经在应用启动的时候存在了,那么设置会被加载并且赋到 `AppSettings` 共享实例的属性上。 现在再到 `ViewController.swift` 文件,移动到 `tryAppSettings()` 方法中。我们将会修改一些设置: @@ -502,7 +502,7 @@ func tryAppSettings() { } ``` -让我们来更新一下设置文件,然后看看我们写入了什么: +让我们来更新一下设置文件,然后看看写入了什么: ```swift if AppSettings.shared.update() { @@ -512,7 +512,7 @@ if AppSettings.shared.update() { } ``` -上面的代码片段除了 `update()` 之外我们还使用了 `toDictionary()` 方法,并且我们还将文件内容加载进了字典对象。然后我们将字典中除 nil 之外的值打印了出来。 +上面的代码片段除了 `update()` 之外还使用了 `toDictionary()` 方法,并且还将文件内容加载进了字典对象。然后我们将字典中除 nil 之外的值打印了出来。 同时,为了验证文件是否被成功创建,再添加一行代码,它会打印应用的真实的缓存目录,你可以通过 Finder 来查看: @@ -538,11 +538,11 @@ if AppSettings.shared.reset() { } ``` -再次运行。这次你会先看到被修改的值,然后是原始值。这意味着我们重置到初始设置如预期的成功了! +再次运行。这次你会先看到被修改的值,然后是原始值。这意味着重置到初始设置如预期的成功了! ![reset_settings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_5_results_2.png) -让我们切换到 *PlayerSettings.swift* 文件然后找到用来表示虚拟游戏玩家设置的 `PlayerSettings` 结构体。首先让 `PlayerSettings` 结构体采纳 `Codable` 和 `SettingsManageable` 协议: +切换到 *PlayerSettings.swift* 文件然后找到用来表示虚拟游戏玩家设置的 `PlayerSettings` 结构体。首先让 `PlayerSettings` 结构体遵循 `Codable` 和 `SettingsManageable` 协议: ```swift struct PlayerSettings: Codable, SettingsManageable { @@ -552,7 +552,7 @@ struct PlayerSettings: Codable, SettingsManageable { 与 `AppSettings` 类所使用的属性相反,这里初始值并没有直接赋给属性。与之替代的是存在于应用包中的 *PlayerSettings.plist* 文件,你可以在 Project Navigator 中找到。当你打开文件时,注意在 .plist 文件中的键名和属性名相同。 -回到 `ViewController.swift` 文件,找到 `tryPlayerSettings()` 方法。要做的第一件事是初始化一个 `PlayerSettings` 对象,然后加载设置,但这一次我们将会使用 `loadUsingSettingsFile()` 方法。同样的,我们也会像之前那样将结果打印出来: +回到 `ViewController.swift` 文件,找到 `tryPlayerSettings()` 方法。要做的第一件事是初始化一个 `PlayerSettings` 对象,然后加载设置,但这一次将会使用 `loadUsingSettingsFile()` 方法。同样的,我们也会像之前那样将结果打印出来: ```swift func tryPlayerSettings() { @@ -566,13 +566,13 @@ func tryPlayerSettings() { } ``` -下面是我们运行应用之后打印在控制台的结果: +下面是运行应用之后打印在控制台的结果: ![console2](https://www.appcoda.com/wp-content/uploads/2019/08/t67_6_results_3.png) 以及这是与 *AppSettings.plist* 同时存在的 *PlayerSettings.plist* 文件: ![PlayerSettings](https://www.appcoda.com/wp-content/uploads/2019/08/t67_7_files2.png) -让我们来修改一些属性,然后更新 .plist 文件,并且再次打印: +再来修改一些属性,然后更新 .plist 文件,并且再次打印: ```swift playerSettings.isMale = false @@ -592,7 +592,7 @@ if let dictionary = playerSettings.toDictionary() { 这里是从 .plist 文件中获取到的更新后的设置: ![updated_plist](https://www.appcoda.com/wp-content/uploads/2019/08/t67_8_results_4.png) -最后,让我们尝试着重置设置,然后验证一下原始的设置文件是否会覆盖当前在缓存目录中的设置文件: +最后,来尝试着重置设置,然后验证一下原始的设置文件是否会覆盖当前在缓存目录中的设置文件: ```swift _ = playerSettings.reset() @@ -603,6 +603,6 @@ if let dictionary = playerSettings.toDictionary() { ![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) # 总结 -我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能性以及对于所有应用来说都很必要的自动化任务。同时,利用了我们这里提到的解决方案,增加了工作的效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! +我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能以及提供所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! -作为参考,你可以在 GitHub 上下载到[完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file +作为参考,你可以在 GitHub 上下载到 [完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From 2220c8a6e0e4a86ad87d6ab43d19cbaf97fa27f4 Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Wed, 8 Jan 2020 14:59:33 +0800 Subject: [PATCH 17/20] update swift-protocols-app-configuration(v2) --- .../20191220_swift-protocols-app-configuration.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index e5e8f0d4..b671a25f 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -16,23 +16,23 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 定稿= -大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我不认为有任何一个程序员不知道协议。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: +大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我认为不会有不知道协议的程序员。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: > 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是满足了该协议。 用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 -让协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了他们的功能性。 +协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! # 线路图 -我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,随着我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 -在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来对其扩展更深一步,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议进行改造。在你下载之后,请用 Xcode 打开然后往下阅读。 @@ -51,7 +51,7 @@ protocol SettingsManageable { } ``` -我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能性。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: +我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: ```swift extension SettingsManageable where Self: Codable { @@ -319,7 +319,7 @@ func delete() -> Bool { 为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得是第一次在缓存目录创建的文件。我们将会以它来重置设置为初始值,并且这样的话,也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 -为了达到这个目的,还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望他们可以被显式调用,所以不会将它们定义在协议中。取而代之的是在协议扩展中方法实现的地方将它们标记为 `private`。第一个方法是将原始文件备份: +为了达到这个目的,还需要两个方法;一个用于备份 .plist 文件,一个用于恢复它。因为这两个方法算是某种“内部”操作,我们并不希望它们可以被显式调用,所以不会将它们定义在协议中。取而代之的是在协议扩展中方法实现的地方将它们标记为 `private`。第一个方法是将原始文件备份: ```swift private func backupSettingsFile() { @@ -418,7 +418,7 @@ mutating func reset() -> Bool { 如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! # 将属性列表内容当作字典 -所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JOSN,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 +所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: From 4fbb9c1b51a2373b330f0ad41f8d6dcdd6e7657c Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Thu, 30 Jan 2020 14:26:19 +0800 Subject: [PATCH 18/20] update swift-protocols-app-configuration(v3) --- ...91220_swift-protocols-app-configuration.md | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index b671a25f..8016ab32 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -16,12 +16,12 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 定稿= -大家好欢迎阅读这篇新教程!协议是广大程序员们在使用 Swift 时最常接触并且使用的概念之一,并且我认为不会有不知道协议的程序员。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: +大家好,欢迎阅读这篇新教程!协议是广大程序员在使用 Swift 时最常接触和使用的概念之一,我认为不会有不知道协议的程序员。协议常常被用于各种目的,但被大家记住的永远都是苹果官方文档中所描述的: -> 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是满足了该协议。 +> 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是遵循了该协议。 -用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议采用类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 +用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议遵循类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 @@ -29,17 +29,21 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! +通过协议定义一系列负责文件处理的方法,以及通过协议扩展提供这些方法的默认实现。这样一来每一个遵循了协议的类或结构体,都能够获得相同的文件相关的功能。不仅如此,支持了各种设置的类或结构体还能以这种方式同时存在于应用中,这些数据也能被分别处理。换句话说,这种协议就像是某种即插即用机制,可以使任何遵循它的自定义类型具备保存和读取数据的能力。 + +这是我长期以来都在使用的解决方案,现在是时候将它拿出来在这讨论了。这篇文章所讨论的内容很显然是针对 Swift 开发新人的,可以让他们了解协议,以及协议的实际用法。毫无疑问高级开发者们早已有了类似的解决方法,但无论你处于什么阶段,都推荐继续阅读下去。 + # 线路图 -我们今天的目标是创建一个可以被任何类型采用,支持简单地保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**,我们一步一步地从零开始,最终会让它完全正确地运作。然而要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是如何通过不同地方式来保存各种的类或结构体的数据。为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑设置的实现时,编辑一个属性列表通常会是第一个出现在脑海中的方案。为了更好玩,我们要加上对应用包中默认初始设置处理的支持(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +我们今天的目标是创建一个可以被任何类型遵循,支持简单的保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 -在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里是处理的属性列表数据,也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型比如 JSON 或是纯文本。 +在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里处理的是属性列表数据,但也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型,比如 JSON 或是纯文本。 -最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议进行改造。在你下载之后,请用 Xcode 打开然后往下阅读。 +最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议对其改造。在你下载之后,请用 Xcode 打开然后往下阅读。 # 开始:创建协议 -让我们打开启动项目,然后创建一个将用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 +让我们打开启动项目,然后创建一个用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 -下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 Create 按钮,来让 Xcode 真正地创建新文件。 +下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 *Create* 按钮,来让 Xcode 真正地创建新文件。 > 注意: 这篇文章中的代码是在 Xcode 10.3 中创建的,这是在撰写这篇文章时最新且最稳定的 Xcode 版本。 @@ -51,7 +55,7 @@ protocol SettingsManageable { } ``` -我之前简单地解释过,我们将会通过对 `SettingsManageable` 协议中定义的方法,进行默认的行为实现,这样可以通过遵循协议来得到我们想要的功能。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: +我之前简单地解释过,我们将会在 `SettingsManageable` 协议中定义方法,然后提供默认的行为实现,这样可以通过遵循协议来得到我们想要的功能。在明确这一点之后,直接在上面这个协议的花括号后面加上几个空行,然后定义它的扩展: ```swift extension SettingsManageable where Self: Codable { @@ -63,10 +67,10 @@ extension SettingsManageable where Self: Codable { 为什么我们需要 `Codable`?因为我们会对遵循了 `SettingsManageable` 的自定义类型中的值及该类型的属性列表数据,进行*编码*和*解码*。 -以免有的类型并未遵循 `Codable` 但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 +这样可以避免有的类型没有遵循 `Codable`,但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 # 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供相应的实现。话不多说,让我们开始吧! +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! ## 获取文件 URL 任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! @@ -95,7 +99,7 @@ extension SettingsManageable where Self: Codable { 注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 ## 存储(更新)一个 SettingsManageable 实例 -毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: +毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 2. 写入文件 @@ -109,9 +113,9 @@ protocol SettingsManageable { } ``` -将其命名为 `update()`,正是由于其作用就是*在有任何修改时都去更新保存了设置的文件*。其底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否成功。 +将其命名为 `update()`,是因为其作用就是*在有任何修改时都去更新保存了设置的文件*。它的底层实现是,每次方法被调用时,所有的属性都将被编码后写入文件。同时这个方法还将返回 true 或 false 来让我们知道是否更新成功。 -我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 对象转化成 JSON。有很大概率你已经用过了它!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? +我猜你应该听过 [**JSONEncoder**](https://developer.apple.com/documentation/foundation/jsonencoder) 这个类,它的作用是可以简单地将任何符合 Codable 的对象转化成 JSON。大概率你已经用过它了!然而,你知道除了 JSONEncoder 之外,**还有一个叫 [PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder) 的类**可以将*任何符合 Codable 的对象编码后转化成属性列表对象吗*? 噢,是的,就是有这么一个类,而且我们将要用上它!在协议扩展中,先写上如下代码: @@ -129,7 +133,7 @@ func update() -> Bool { 如你所见,真正起作用的是这句:`PropertyListEncoder().encode(self)`。首先创建了一个 `PropertyListEncoder` 的类,然后调用了 `encode(_:)` 方法来对 `self`(遵循了 `SettingsManageable` 的自定义类型)进行编码转化成一个属性列表对象,实际上它就是一个 `Data` 对象! -如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里我们并未对异常做任何额外操作,我们只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 +如果编码失败那么 `encode:` 方法会在错误发生时抛出一个异常,所以有必要在 `do-catch` 语句中使用 `try` 关键字。注意这里并未对异常做任何额外操作,只是打印了错误然后返回了 false。你可以根据需要来决定是否要采取额外的操作。 上面的代码并没有将编码后的数据*写入*文件。在 `do` 中,再加上这么一行: @@ -158,7 +162,7 @@ func update() -> Bool { } ``` -在我们继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望方法内部所使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: +在继续之前,让我再给你展示*另一种上述方法的实现方式*。假设我们不需要布尔返回值。取而代之的是,我们希望方法内部所使用的方法们能够自己抛出异常。这样的话,首先需要修改 `SettingsManageable` 协议的定义: ```swift func update() throws @@ -212,7 +216,7 @@ mutating func load() -> Bool { } ``` -我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次需要使用 `path` 属性来获取路径字符串。当确定文件存在之后,就从文件中加载内容到 `fileContents` 实例中,拿到加载的数据后,通过 `PropertyListDecoder` 类解码后,对 `self`(遵循了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,返回 true,如果出现了错误则会抛出异常,然后将错误打印在控制台并且返回 false。 +我们又一次使用了 `settingsURL()` 方法来获取文件 URL。然而这次需要使用 `path` 属性来获取路径的字符串。当确定文件存在之后,就从文件中加载内容到 `fileContents` 实例中,拿到加载的数据后,通过 `PropertyListDecoder` 类解码后,对 `self`(遵循了 `SettingsManageable` 协议的类型)进行了初始化。如果所有的步骤都成功了,返回 true,如果出现了错误则会抛出异常,然后将错误打印在控制台并且返回 false。 现在方法还没完全实现,当 `if` 判断为 false 的时候,意味着文件并不存在,这种情况还没有返回值。如果没有文件可以加载设置,那么就创建一个!怎么创建?通过调用上一步我们实现的 `update()` 方法! @@ -229,7 +233,7 @@ mutating func load() -> Bool { 现在方法在所有的情况下都有返回值了。如果是第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 ## 通过应用包中的 Plist 文件初始化设置 -既然我们一直在聊设置,那么很自然就会想到它们的的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该在被分配初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: +既然我们一直在聊设置,那么很自然就会想到它们的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该有初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: *属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* @@ -283,10 +287,10 @@ do { } catch { ... } ``` -无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常工作。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 +无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常运行。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 ## 删除设置文件 -对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: +对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了达到这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: ```swift protocol SettingsManageable { @@ -315,7 +319,7 @@ func delete() -> Bool { ## 重置设置 如果有办法可以在任意时间切换回最初的设置,那将会是个很有用的功能。这背后的含义其实是删除缓存目录中的属性列表文件,然后再把初始值写回去。 -当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,当初始值被赋给了属性,事情就变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,又重新保存到文件中,这显然是错误的! +当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,初始值已经被赋值给了属性,这就让事情变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,然后又重新保存到文件中,这显然是错误的! 为了解决这个问题,在覆盖之前,我们需要保存一份原始设置的拷贝。这份拷贝得是第一次在缓存目录创建的文件。我们将会以它来重置设置为初始值,并且这样的话,也不需要在意是否之前的设置已经被加载了,可以避免又重写回去的风险。 @@ -374,7 +378,7 @@ mutating func load() -> Bool { } ``` -现在我们可以专注到重置方法的实现啦。在 `SettingsManageable` 协议中添加: +现在我们可以专注于重置方法的实现啦。在 `SettingsManageable` 协议中添加: ```swift protocol SettingsManageable { @@ -396,7 +400,7 @@ mutating func reset() -> Bool { } ``` -下一步要小心一点。为了让 `reset()` 方法与加载设置的方法独立开来。因此,我们要首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的的初始设置备份文件中恢复,然后调用 `load()` 方法来加载设置。 +下一步要小心一点。为了让 `reset()` 方法与加载设置的方法独立开来。因此,我们要首先通过 `loadUsingSettingsFile()` 方法来尝试着从应用包中设置文件加载初始设置。如果成功了,初始设置则会被拷贝到缓存目录中。如果失败了,则是由于应用包中并没有相关文件,那么我们会再尝试着使用 `restoreSettingsFile()` 方法,来从之前的初始设置备份文件中恢复,然后调用 `load()` 方法来加载设置。 这就是上面所描述「场景」的代码: @@ -415,10 +419,10 @@ mutating func reset() -> Bool { } ``` -如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是通过应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! +如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! # 将属性列表内容当作字典 -所有 `SettingsManageable` 协议中方法的目的,都是为所有遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的机会,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 +所有在 `SettingsManageable` 协议中的方法,都是为遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的次数,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: @@ -430,7 +434,7 @@ protocol SettingsManageable { } ``` -这个方法会返回一个范型为 `[String: Any?]` 的字典,或者是 nil,如果属性列表文件在被解码的过程中,发生了如无法解码这样糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 +这个方法会返回一个范型为 `[String: Any?]` 的字典,或者是 nil,避免属性列表文件在被解码的过程中发生了如无法解码这样糟糕的事情。注意返回的字典值的数据类型被定义成了 `Any?`,这就覆盖了值是 nil 的情况。 在协议扩展中我们将会提供一个上述方法的默认实现,第一步就是检查属性列表文件是否存在,这个之前已经做过好几次了。同时还要把文件内容加载到 `Data` 对象中: @@ -460,7 +464,7 @@ let dictionary = try PropertyListSerialization.propertyList(from: fileContents, let dictionary = try PropertyListSerialization.propertyList(from: fileContents, options: .mutableContainersAndLeaves, format: nil) as? [String: Any?] ``` -`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。自从有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就变小了。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。既然我们正在讨论属性列表,就可以顺便提及此方法。 +`PropertyListSerialization` 类和 `**JSONSerialization**` 类差不多,只是处理的是属性列表(如果对你来说 `JSONSerialization` 要更熟悉一些的话)。自从有了 Swift 提供的 `Codable` 协议,使用 `PropertyListSerialization` 类的几率就变小了。大部分情况下你会用 `PropertyListEncoder` 和 `PropertyListDecoder` 类。但既然我们正在讨论属性列表,就可以顺便提及此方法。 现在要怎么通过 `PropertyListSerialization` 从一个字典获取到属性列表文件呢? @@ -469,7 +473,7 @@ let dictionary = try PropertyListSerialization.propertyList(from: fileContents, # SettingsManageable 协议实战 是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! -首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你第一件注意到的事就是这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意。会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你马上会注意这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意,会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 我们需要做的第一件事就是让 `AppSettings` 类遵循 `SettingsManageable` 协议。不要忘了也要遵循 `Codable` 协议: @@ -479,7 +483,7 @@ class AppSettings: Codable, SettingsManageable { } ``` -因为我们一直讨论的是应用的设置,所以有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过之前实现的 `load()` 方法来加载设置: +因为我们一直讨论的是应用的设置,那就有必要在应用启动的时候马上就能拿到它们。所以,打开 *AppDelegate.swift* 文件,然后找到 `application(_:didFinishLaunchingWithOptions:)` 方法。修改它,然后通过之前实现的 `load()` 方法来加载设置: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -520,11 +524,11 @@ if AppSettings.shared.update() { print(AppSettings.shared.settingsURL().path) ``` -如果你运行应用,你就能在控制台中看到如下输出: +如果你运行应用,就能在控制台中看到如下输出: ![console](https://www.appcoda.com/wp-content/uploads/2019/08/t67_3_results_1.png) -如果你通过 Finder 跳转到打印出来的路径,你还能看到有两个文件被创建: +如果你通过 Finder 跳转到打印出来的路径,你还能看到创建了两个文件: ![files_created](https://www.appcoda.com/wp-content/uploads/2019/08/t67_4_files1.png) @@ -603,6 +607,6 @@ if let dictionary = playerSettings.toDictionary() { ![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) # 总结 -我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能以及提供所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以想象如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! +我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能,以及协议如何提供了所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以再想想如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! 作为参考,你可以在 GitHub 上下载到 [完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From a5268fba3f88d16ddec8aaf765b6b753d1228789 Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Fri, 14 Feb 2020 11:28:13 +0800 Subject: [PATCH 19/20] update swift-protocols-app-configuration(v4) --- ...91220_swift-protocols-app-configuration.md | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index 8016ab32..4a4261c2 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -21,7 +21,7 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 > 协议是定义方法,属性和其他符合某种特殊任务要求的蓝图。协议可以被类,结构体或是枚举采用,根据其定义提供符合要求的实现。任何类型只要满足了协议的要求,就可以被称为是遵循了该协议。 -用更简单的话来说,一个 Swift 协议定义了一些方法和属性,需要由协议遵循类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 +简而言之,一个 Swift 协议定义了一些方法和属性,需要由协议遵循类型(类,结构体,枚举)来实现。定义的这些方法和属性被称为*约定*。 协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 @@ -29,18 +29,18 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! -通过协议定义一系列负责文件处理的方法,以及通过协议扩展提供这些方法的默认实现。这样一来每一个遵循了协议的类或结构体,都能够获得相同的文件相关的功能。不仅如此,支持了各种设置的类或结构体还能以这种方式同时存在于应用中,这些数据也能被分别处理。换句话说,这种协议就像是某种即插即用机制,可以使任何遵循它的自定义类型具备保存和读取数据的能力。 +通过协议定义一系列负责文件处理的方法,以及通过协议扩展提供这些方法的默认实现。这样一来每一个遵循了协议的类或结构体,都能够获得相同的文件相关的功能!不仅如此,支持了各种设置的类或结构体还能以这种方式同时存在于应用中,这些数据也能被分别处理。换句话说,这种协议就像是某种即插即用机制,可以使任何遵循它的自定义类型具备保存和读取数据的能力。 这是我长期以来都在使用的解决方案,现在是时候将它拿出来在这讨论了。这篇文章所讨论的内容很显然是针对 Swift 开发新人的,可以让他们了解协议,以及协议的实际用法。毫无疑问高级开发者们早已有了类似的解决方法,但无论你处于什么阶段,都推荐继续阅读下去。 -# 线路图 -我们今天的目标是创建一个可以被任何类型遵循,支持简单的保存以及从文件中加载其数据的协议。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +## 线路图 +今天的目标是创建一个协议,让遵循它的类型能以文件的形式很方便的读取或写入。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里处理的是属性列表数据,但也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型,比如 JSON 或是纯文本。 最后,这里有一个供你下载的 [启动项目](https://github.com/appcoda/AppSettings/raw/master/starter.zip)。在这个项目中,你可以找到一些表示设置的自定义类型,我们将会用协议对其改造。在你下载之后,请用 Xcode 打开然后往下阅读。 -# 开始:创建协议 +## 开始:创建协议 让我们打开启动项目,然后创建一个用于实现协议的新文件来开始实践吧。在 Xcode 中,在键盘上按下 *Command+N* 或者通过选择菜单栏 *File > New > File...*。选择 *Swift File* 作为文件模版,然后点击 Next 按钮。 下一步,你必须给这个文件命名。用协议的名字来对其命名:**SettingsManageable**。然后按下 *Return* 或点击 *Create* 按钮,来让 Xcode 真正地创建新文件。 @@ -69,10 +69,10 @@ extension SettingsManageable where Self: Codable { 这样可以避免有的类型没有遵循 `Codable`,但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 -# 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体能够自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! +## 定义并且实现协议的要求 +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! -## 获取文件 URL +### 获取文件 URL 任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! 向协议中添加下面方法的定义: @@ -94,11 +94,11 @@ extension SettingsManageable where Self: Codable { } ``` -第一行从缓存目录获取到了 URL。第二行将*遵循了* ***`SettingsManageable` 自定义类型****添加*到了 URL 末尾,还加上了 “.plist” 扩展,然后我们将其返回。 +第一行从缓存目录获取到了 URL。第二行将**遵循了 `SettingsManageable` 自定义类型添加到了 URL 末尾**,还加上了 **“.plist” 扩展**,然后我们将其返回。 注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 -## 存储(更新)一个 SettingsManageable 实例 +### 存储(更新)一个 SettingsManageable 实例 毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 @@ -179,9 +179,9 @@ func update() throws { 可以看到在这种情况下无需使用 `do-catch` 声明。由于我们将其标记了 `throws` 关键字,这个方法会将任何可能发生的错误传递出去。这意味着每一次调用它我们都需要对其进行异常的检查。我个人觉得这不太实用,我的期望是能够尽量轻松,并且没有额外操作的情况下更新配置。一个能够表示更新成功与否的标记应该足够了,所以我会选择那个方案。然而,为了让你方便,我把这个解决方案也提供给你,供你来选择。注意接下来这个方案不会再出现了,不过你仍然可以通过我刚刚展示的方式来将其转化成这个方案。 -> 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html) 吧! +> 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://swiftgg.gitbook.io/swift/swift-jiao-cheng/17_error_handling) 吧! -## 通过文件加载设置 +### 通过文件加载设置 现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。我们要跟随的步骤很简单,只需要跟着下面这几步: 1. 检查文件是否存在。 @@ -232,7 +232,7 @@ mutating func load() -> Bool { 现在方法在所有的情况下都有返回值了。如果是第一次使用应用,或是文件被删除了,导致的文件不存在,那么通过调用 `update()` 方法会创建一个。如果存在,那么它会加载其内容,将其解码然后利用解码后的数据来初始化对象。 -## 通过应用包中的 Plist 文件初始化设置 +### 通过应用包中的 Plist 文件初始化设置 既然我们一直在聊设置,那么很自然就会想到它们的默认值。在编程层面,遵循了 `SettingsManageable` 的类或者是结构体的属性都应该有初始值或者说默认值。然而这并不是唯一实践或者最适合的方式。通过 Xcode 的编辑器编辑应用包内的属性列表文件来初始化也许是是更好且简单的方式。我们接下来将实现这种方式,并遵循**一条很重要的规则**: *属性列表文件中的键,应该和那些表示着设置的类或结构体的属性名称相同!* @@ -289,7 +289,7 @@ do { 无论缓存目录里是否存在设置文件,我们的逻辑实现都能正常运行。如果文件不存在,那么会先从应用包中拷贝到缓存目录,然后内容将会被解码,用于初始化设置对象。 -## 删除设置文件 +### 删除设置文件 对自定义类型进行编码然后写入属性列表文件,然后再读取的流程已经完成了。接下来我们必须提供一种让文件能被轻松移除的方法。为了达到这个目的,我们将在 `SettingsManageable` 协议中添加如下方法: ```swift @@ -316,7 +316,7 @@ func delete() -> Bool { 如果移除文件像上面这样成功了,那么方法会返回 true。如果在移除过程中产生了任何错误,那么返回 false。 -## 重置设置 +### 重置设置 如果有办法可以在任意时间切换回最初的设置,那将会是个很有用的功能。这背后的含义其实是删除缓存目录中的属性列表文件,然后再把初始值写回去。 当原始设置存在于应用包中的 .plist 文件时,将设置重置为初始值就是个非常简单直接的事情了。直接删除缓存目录中的 .plist 文件,然后再将原始的文件拷贝放回去就行了。然而在应用包中也不存在于原始文件的情况下,初始值已经被赋值给了属性,这就让事情变得有些棘手了!因为在重置的时候,已经被加载的值会将默认值替换了,然后又重新保存到文件中,这显然是错误的! @@ -421,8 +421,8 @@ mutating func reset() -> Bool { 如果 `loadUsingSettingsFile()` 方法返回 true,那么执行则会走到里面的 `else`,并且方法会返回 true。另一种情况,如果恢复初始设置成功了,则会返回 `load()` 方法的执行结果。无论初始设置是如何被定义的,是直接设置给属性,还是应用包中的属性列表文件,上面的实现都会正常工作。你或许会想为了实现一个重置的功能费这么多功夫是否值得,但相信我,值。随着时间的推移,你绝对会需要这个功能,而那时候就可没有那么多时间来实现了! -# 将属性列表内容当作字典 -所有在 `SettingsManageable` 协议中的方法,都是为遵循了协议的类或结构体,提供对应的保存和加载的方法。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的次数,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,那就让我们来更进一步地使用它,来看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 +## 将属性列表内容当作字典 +上述这些 SettingsManageable 协议的方法,为遵循了协议的类或结构体提供了保存与加载的能力。数据保存是通过 `PropertyListEncoder` 编码后存储为属性列表文件,然后通过 `PropertyListDecoder` 来解码。诚然,与属性列表数据和其相关的类打交道的次数,并不像遇到 JSON,`JSONEncoder` 和 `JSONDecoder` 那么多。所以借此机会,让我们来更进一步地使用它,看看怎么使用 `PropertyListSerialization` 类以及如何将属性列表作为字典类型获取。 在这里为了演示,我们将会在 `SettingsManageable` 中再定义一个方法: @@ -470,10 +470,10 @@ let dictionary = try PropertyListSerialization.propertyList(from: fileContents, 嘿,我将这个问题留给你作为可选的练习。给一个小提示,如果是我的话,会去找一个叫 `data(fromPropertyList:format:options)` 的方法。 -# SettingsManageable 协议实战 +## SettingsManageable 协议实战 是时候来用用我们完成的实现了。在启动项目中你可以找到两个测试文件,分别是叫做 *AppSettings* 的类和叫做 *PlayerSettings* 的结构体。我们将会通过它们来看看我们的协议在实践中如何使用的! -首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。在这个类中,你马上会注意这个类有一个静态的共享实例,以及一个私有的初始化方法,它是个符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的类。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意,会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 +首先从 *AppSettings.swift* 文件开始,`AppSettings` 类中模拟了几个能在真实应用中找到的设置选项。你会注意到,这个类有一个静态的共享实例,以及一个私有的初始化方法,它符合 [***单例***](https://en.wikipedia.org/wiki/Singleton_pattern) 的特性。我选择这种方式的理由很简单:当处理应用的设置时,创建多个实例来持有这些设置,可能不是一个好的主意,会存在数据重载的风险。而使用单例时,因为只有一个实例,搞坏设置数据的风险被降到了最低。然而如果你不同意这种方案,可以自行移除 `init()` 方法的 `private`关键字,删除静态共享实例,然后在需要的时候,像其他普通的类一样初始化创建实例。我们会在下一个例子中看到如此处理的 `PlayerSttings` 结构体。同时你会注意到设置的初始值是默认的属性值。 我们需要做的第一件事就是让 `AppSettings` 类遵循 `SettingsManageable` 协议。不要忘了也要遵循 `Codable` 协议: @@ -606,7 +606,7 @@ if let dictionary = playerSettings.toDictionary() { ``` ![reset](https://www.appcoda.com/wp-content/uploads/2019/08/t67_9_results_5.png) -# 总结 +## 总结 我们终于要结束这篇文章了。希望你喜欢这篇文章,并在今天学到了有用的东西。这篇文章所呈现的概念很简单,但也清晰地展示了协议与扩展是如何为其他类型提供额外的功能,以及协议如何提供了所有应用都必要的自动化任务。同时,使用上我们这里提到的解决方案,能提高工作效率,你也不需要担心像是如何保存以及加载设置这样的小事,有了更多处理重要任务的时间。现在既然你已经读到了这里,可以再想想如何改进你自己的任务。这或许要比你想象的更简单。再次感谢阅读,我们很快还会再见的! 作为参考,你可以在 GitHub 上下载到 [完整的项目](https://github.com/appcoda/AppSettings)。 \ No newline at end of file From ca08a08f8872ed24778cf80e14d6ba64e95e129d Mon Sep 17 00:00:00 2001 From: Yu Wang Date: Fri, 21 Feb 2020 17:24:22 +0800 Subject: [PATCH 20/20] patch swift-protocols-app-configuration(v4) --- appcoda/20191220_swift-protocols-app-configuration.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/appcoda/20191220_swift-protocols-app-configuration.md b/appcoda/20191220_swift-protocols-app-configuration.md index 4a4261c2..f0b646c5 100644 --- a/appcoda/20191220_swift-protocols-app-configuration.md +++ b/appcoda/20191220_swift-protocols-app-configuration.md @@ -25,7 +25,7 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 协议特别有趣的一点在于,可以简单地通过对协议提供扩展,来提供默认实现的能力。事实上正是这个功能,让协议可以如此强大并且成为 Swift 开发中流行话题的原因。通过定义一组方法来描述一系列功能,并对它们提供基础的实现。甚至可以让与其无关的类,结构体,枚举类型(与之相反的例子是类的继承)也能够获得一个通用的附加功能,这些附加能力扩展了它们的功能性。 -如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html),你可以在那里发现有许多有趣的信息。 +如果你是一个新手开发者,那么我强烈推荐你阅读 [**更多关于协议的信息**](https://swiftgg.gitbook.io/swift/swift-jiao-cheng/21_protocols),你可以在那里发现有许多有趣的信息。 好了,那到底协议与这篇文章标题所说的应用设置有什么关系吗?让我来解释一下其中的联系。很久以来,我一直都因 [**UserDefaults**](https://developer.apple.com/documentation/foundation/userdefaults) 是唯一可以快速地存储少量数据的机制而感到苦恼。毋庸置疑 User Defaults 是挺好的,但这是一种笼统地并且不 “Swifty” 的方案(说实话,我已经很少使用它们了)。我需要的是一种能够为我的每个应用量身定做的解决方案。迈向这个目标的第一步很简单:创建一个包含了应用配置和用户偏好的类或者是结构体。然而这个方案的缺点在于所有的这些选项,都需要对应的文件操作方法(保存,加载,删除)。方法应该只编写*一次*,就能够在*任何*地方被*任何类型*使用。通过类和继承来实现这些基础功能的想法可以直接抛弃了,因为这会阻碍我们使用结构体(`struct`)。这就是协议大展身手的时候了! @@ -34,7 +34,7 @@ description: 本文详细讲解了如何利用 Swift 协议来管理应用配置 这是我长期以来都在使用的解决方案,现在是时候将它拿出来在这讨论了。这篇文章所讨论的内容很显然是针对 Swift 开发新人的,可以让他们了解协议,以及协议的实际用法。毫无疑问高级开发者们早已有了类似的解决方法,但无论你处于什么阶段,都推荐继续阅读下去。 ## 线路图 -今天的目标是创建一个协议,让遵循它的类型能以文件的形式很方便的读取或写入。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,直到最后让它完全正确地运作。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 +今天的目标是创建一个协议,让遵循它的类型能以文件的形式很方便的读取或写入。将其命名为 **SettingsManageable**。我们将一步一步地从零开始,到最后完全实现所有功能。但是要记住,我们的重点是如何轻松地处理应用的设置以及设置相关的通用类型,并不是实现如何通过不同地方式来保存各种的类或结构体的数据。所以为了达成这个目的,接下来将要创建的文件是 *属性列表*(.plist)。毕竟当考虑到设置的实现时,编辑一个属性列表通常会是首先出现在脑海中的方案。为了更好玩一些,我们要加上对应用包中默认初始设置文件的处理(那些可以在 Xcode 属性列表编辑器中编辑的设置)。 在完成实现之后,我们还会看到一些如何使用这个协议的简单示例。虽然在这里处理的是属性列表数据,但也欢迎你来进行更深入的扩展,让协议更通用,让它可以支持任何自定义类型以及其他的数据类型,比如 JSON 或是纯文本。 @@ -70,7 +70,7 @@ extension SettingsManageable where Self: Codable { 这样可以避免有的类型没有遵循 `Codable`,但遵循了 `SettingsManageable`,导致我们接下来无法为其提供正确的功能。 ## 定义并且实现协议的要求 -这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体自动地保存及加载自身。因此我们作为开发者,可以在没有任何代价的情况下,将它们当作是设置或偏好选项来对其改动或是更新。从现在起,我们将会定义协议中的每一个方法,并且我们会在协议扩展中提供对应的实现。话不多说,让我们开始吧! +这是这篇文章最有趣的部分了,当我们实现了所有的方法之后,就能够让任何遵循了 `SettingsManageable` 的类或结构体自动地保存及加载自身。因此,我们可以将这些类和结构体作为设置或者偏好选项进行改动或是更新,而不需要做额外的变动。从现在起,我们将会定义协议中的每一个方法,并且会在协议扩展中提供对应的实现。话不多说,让我们开始吧! ### 获取文件 URL 任何遵循了 `SettingsManageable` 协议的自定义类型(类,结构体,甚至是枚举)的值都会被存储在属性列表文件中。我们会将这些文件保存在应用的 *Caches* 目录下,这也将是我们的起点:获取到设置文件的 URL! @@ -99,7 +99,7 @@ extension SettingsManageable where Self: Codable { 注意 `\(Self.self)` 动态地提供了遵循 `SettingsManageable` 的类型的名字。这挺酷的,因为这样每个类或结构体都会有属于自己名字的 .plist 文件存储在缓存目录下,并且不会重复。举例来说,一个叫 “Settings” 的类遵循了 `SettingsManageable`,那么就会在缓存目录中创建一个叫做 “Settings.plist” 的文件,当有一个叫 “AppColors” 的结构体遵循了 `SettingsManageable` 协议,那么也就会有一个 “AppColors.plist” 的文件。 ### 存储(更新)一个 SettingsManageable 实例 -毋庸置疑上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: +毋庸置疑,上面实现的方法是非常实用的,但是它也只是一个为了方便的辅助方法,我们完全可以在没有它的情况下处理事情。接下来才是第一个必备的方法,我们将要处理一些真正重要的事情:将遵循了协议的类型的所有属性保存到属性列表文件中。“保存”这个词涵盖了两种不同的行为: 1. 编码为属性列表 2. 写入文件 @@ -182,7 +182,7 @@ func update() throws { > 注意:想要了解更多关于错误处理的内容?看看 [这个文档](https://swiftgg.gitbook.io/swift/swift-jiao-cheng/17_error_handling) 吧! ### 通过文件加载设置 -现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。我们要跟随的步骤很简单,只需要跟着下面这几步: +现在让我们过渡到通过文件加载数据,对应刚刚提到的保存数据部分。只需要照着以下几个步骤来就可以了: 1. 检查文件是否存在。 2. 将文件内容加载为 `Data` 对象。