Icon

RVS_PersistentPrefs

一个通用的 Swift 类,可以极其简单和透明地存储持久性偏好设置。

以下是该类的技术文档。

以下是该类的 GitHub 仓库。

概述

RVS_PersistentPrefs 是一个“抽象”基类,旨在被子类化(是的,我知道。Swift 中没有“抽象”类这样的东西,但是如果您单独实例化该类,它会被设计为崩溃)

基于该类的实例将具有一个简单、灵活的 Dictionary<String, Any> 属性。这将包含一组代表派生子类存储的值。

基类还会从 UserDefaults 透明地保存和检索 Dictionary

每个实例还将具有一个名为 key 的存储属性,它是一个 String,用于“键控”存储的数据。

这意味着基于 RVS_PersistentPrefs 的子类的多个实例可以跟踪多组持久性数据。

RVS_PersistentPrefs 旨在易于使用和可靠。它不是一个用于存储任务关键型或大量数据的类。它仅仅是一种维护少量持久性数据的便捷方式,例如应用程序首选项。

该类解决了什么问题?

存储持久性数据(在应用程序启动和停止后仍然存在的数据,甚至在某些情况下,可以在应用程序之间传输的数据)一直是应用程序开发中一个有些令人担忧的过程。

幸运的是,Apple 提供了一种极好的机制,称为 UserDefaults,它是 Foundation 框架的一部分;这意味着所有 Apple 操作系统都支持它。

UserDefaults 保存和检索数据非常简单。您可以像处理 String 键控的 Dictionary 一样保存和检索值。

但是,有一个主要的限制:所有存储的数据都需要与 XML plist 兼容。这通常不是什么大问题,因为有很多类型可以正常工作。实际的存储发生在 XML Plist 文件中,因此需要支持存储的类型。

该类将在尝试存储首选项之前“审查”它们(否则,系统只会崩溃),如果出现问题,会通知您。它还完全抽象了与 UserDefaults 的实际接口,因此您只需担心与 Dictionary<String, Any> 交互。

子类还将建立“允许”的键,并且可以执行诸如将存储的值转换为 键值观察者 属性之类的操作,因此您可以设置用户界面和存储的首选项之间的完全“无代码”连接。

该类允许您拥有一个“隐式”全局状态,只需实例化一个子类即可访问它,并且您可以将“顶级键”与实例关联,以维护多组持久性首选项。

要求

RVS_PersistentPrefs 是一个基于 Apple Foundation 的资源。它可以在所有 Apple 开发平台(iOSiPadOSmacOStvOSwatchOS)上同样正常工作。它不能在非 Apple 平台上工作,并且不旨在支持除原生 Swift 开发之外的任何内容。

这需要 Swift 4.0 或更高版本。

安装

您可以使用 SPM 通过引用其 GitHub 仓库 URI(SSH:git@github.com:RiftValleySoftware/RVS_PersistentPrefs.git 或 HTTPS:https://github.com/RiftValleySoftware/RVS_PersistentPrefs.git)来将项目作为依赖项加载。

连接依赖项后,您可以通过将 import 添加到使用该包的文件来引用它。

import RVS_PersistentPrefs

要从 Carthage 使用此功能,只需将以下内容添加到您的 Cartfile

github "RiftValleySoftware/RVS_PersistentPrefs"

然后,您 cd 到项目目录,并在命令行上执行 carthage update

这将导致一个名为“Carthage”的目录。

然后,您需要包含在

Carthage/Checkouts/RVS_PersistentPrefs/RVS_PersistentPrefs/RVS_PersistentPrefs.swift

在您的项目中。没有库或框架。您需要直接引用并包含 Swift 源文件。

您可以从 其 GitHub 仓库 获取最新版本的 RVS_PersistentPrefs

该类由 单个 Swift 源文件 组成。项目中的所有其他内容都用于项目支持和测试。

只需将 此文件 复制到您的项目中,并将其添加到您当前的 Swift 原生目标。

这不需要任何依赖项管理器。这是一个 300 行的文件。绝对不值得甚至为此编写 podfile。它可以在所有 Apple 操作系统上运行,而无需任何其他依赖项。

它确实非常容易使用。包含一个非常小的文件,编写一个简短的子类,一切都搞定了。

重要的实现说明

线程安全

没有。处理它并继续前进。

由于该实用程序的性质(针对少量 - 通常 - 与用户界面相关的数据的“快速而肮脏”的持久性保存),线程安全不是一个关键需求。但是我还是想指出这一点,因此如果您遇到不一致的取消分配崩溃,则不要花费太多时间在靠垫下搜索。在 RVS_Persistent_Prefs_Thread_Tests.swift 文件中有一个被注释掉的测试。如果您取消注释它并重复运行它,您最终会遇到该问题。您还可以提高测试数量,以增加遇到问题的可能性。

它不需要在主线程上,但不应从不同的线程调用它。

必须被子类化

该实现需要是 RVS_PersistentPrefs 的具体子类。至少,您需要使用计算属性覆盖 keys: [String] 计算属性,以返回一个 StringArray,其中包含内部键。您也可以覆盖 key 存储的属性,但可能更容易在 init 中设置基类属性。

存储的数据是无类型的

请记住,数据的内部存储是 Dictionary<String, Any>。这意味着存储几乎就像一种松散类型的语言。您只需更改提供给它的类型即可更改存储的 Dictionary 值的数据类型。

如果您是 PHP 程序员,那就太好了。如果您是 Swift 程序员,那就没那么好了。

子类的工作之一是将此无类型隐藏在将存储的数据强制转换为一致类型的访问器之后。

所有存储的数据必须与 XML-Plist 兼容

由于我们持久性数据的最终存储“桶”位于 plist 中,因此存储数据层次结构中任何位置的所有数据类型都需要以可以序列化为 XML 形式的形式存在。大多数 ObjC(NS 和 CF)类都可以存储在 plist 中,并且您通常可以通过将它们呈现为 Data 值,并通过 NSCoding 实现返回该值来存储不透明类型。

RVS_PersistentPrefs 类将在尝试保存数据之前“审查”您的数据。如果它检测到任何与 plist 不兼容的数据,它将不会保存该数据,并将 lastError 属性设置为 valuesNotPlistCompatible,这将具有关联的数据。该数据将是 StringArray,其中包含违规元素的顶级键(请记住,您可以存储层次结构,但错误只会报告层次结构的顶级)。

您必须使用 keys: [String] 计算属性提供的键

您不能使用未在 keys: [String] 计算属性 中列出的键提交数据。如果您尝试这样做,则 lastError 属性将设置为 incorrectKeys,这将具有关联的数据。该数据将是 StringArray,其中包含不正确的顶级键。

不抛出异常

RVS_PersistentPrefs 类不会抛出异常。但是,在内部,它会。它还提供了 一个错误枚举,其中包含如果发生错误,它将放入 lastError 属性中的错误。

您应该检查 lastError 是否有问题。如果没有问题,它将为 nil。

KVO

虽然不是必需的,但最好使子类成为 键值观察者。您可以通过编写访问器计算属性并声明它们 @objc dynamic 来实现这一点。 RVS_PersistentPrefs 派生自 NSObject,因此不应有任何问题。

您还可以直接观察 RVS_PersistentPrefs.values 属性。当_任何_首选项更改时,它都会改变(因此可能不适用于“挑选”式的观察)。

在一些包含的测试工具应用程序中,我们将使用 KVO(键值观察)。

用法

首先将主源文件包含在您的项目中

为了使用 RVS_PersistentPrefs,您应该将 RVS_PersistentPrefs.swift 通用源文件包含到您的目标中,然后创建 RVS_PersistentPrefs 类的子类,专门用于您的实现。

您_必须_继承 RVS_PersistentPrefs

它不是一个协议。它是一个类。它也并非设计为独立实例化。如果您这样做,它会在首次使用时故意崩溃。

至少,您需要重写 keys 计算属性,以便将键分配给各种存储的属性。以下示例来自 测试工具共享类

•
•
•

/* ################################################################## */
/**
 This is an Array of String, containing the keys used to store and retrieve the values from persistent storage.
 */
 private static let _myKeys: [String] = ["Integer Value", "String Value", "Array Value", "Dictionary Value", "Date Value"]
 
•
•
•
 
/* ################################################################## */
/**
 This is an Array of String, containing the keys used to store and retrieve the values from persistent storage. READ-ONLY
*/
override public var keys: [String] {
    return type(of: self)._myKeys
}

•
•
•

您_应该_提供类型强制访问器

主存储是无类型的。它只是一个简单的 Dictionary<String, Any>,没有对数据类型进行强制。

您应该提供访问器来访问存储的数据,该访问器会强制类型。同样,以下示例来自 测试工具共享类

•
•
•

/* ################################################################## */
/**
 The Integer Value. READ-WRITE
 */
@objc dynamic public var int: Int {
    get {
        if let ret = values[keys[_ValueIndexes.int.rawValue]] as? Int {
            return ret
        } else {
            #if DEBUG
                print("No legal variant of Integer Value")
            #endif
            return 0
        }
    }

    set {
        return values[keys[_ValueIndexes.int.rawValue]] = newValue
    }
}

•
•
•

/* ################################################################## */
/**
 The String Value. READ-WRITE
 */
@objc dynamic public var string: String {
    get {
        let value = values[keys[_ValueIndexes.string.rawValue]] as? String ?? ""
        return value
    }
        
    set {
        return values[keys[_ValueIndexes.string.rawValue]] = newValue
    }
}

•
•
•

还要注意,上面两个访问器都声明为 @objc dynamic。这使它们有资格进行 键值观察。在 Mac OS 中,这使得在用户界面元素和持久首选项之间建立“无代码”连接变得非常简单(事实上,在 macOS 测试工具项目 中,我们对此进行了演示)。

除了这两件事,您在使用此类时几乎不需要做任何事情。您可以提供一个不同的字符串键,以便存储多组首选项。请记住,这与传统使用 UserDefaults 的方式略有不同,在 UserDefaults 中,每个数据项都给出一个单独的键。使用 RVS_PersistentPrefs,每个_组_参数都有一个“根键”。例如,如果您正在使用 iOS Settings.bundle 在设置应用程序中显示首选项屏幕,则无法轻易直接访问 RVS_PersistentPrefs 首选项。通常最好单独管理一组 UserDefaults,以便提供合适的用户体验。 我们在 iOS 测试工具应用程序中对此进行了演示

您可以添加自己的初始化器

您可能还需要设置一个自定义 init()。在我们的例子中,我们设置了一个 来允许我们设置一个键

•
•
•

/* ################################################################## */
/**
 The keyed initializer. It sends in our default values, if there were no previous ones. Bit primitive, but this is for test harnesses.
 */
public init(key inKey: String) {
    super.init(key: inKey)  // Start by initializing with the key. This will load any saved values.
    if values.isEmpty { // If we didn't already have something, we send in our defaults.
        values = type(of: self)._myValues
    }
}

一旦设置好所有这些,用法就非常简单。您只需通过访问器进行读取和写入。与 UserDefaults 的存储和加载完全在后台透明地处理。它非常健壮,存储在保存数据或读取访问器后立即发生。考虑到这一点,您应该意识到“保存是为了保持”。没有存储在缓存中,然后刷新缓存。完整存储立即发生。

不用说,这有利于健壮性而不是效率。不建议将单个键用于可能由许多部分组成的数据。通常最好将其作为一次存储或获取的 ArrayDictionary,放在一个键下。

测试工具项目

包含许多测试工具应用程序。这些涵盖 iOS/iPadOS、macOS、watchOS 和 tvOS。

所有应用程序都是可本地化的

测试工具应用程序都是完整的、生产质量的应用程序,旨在演示首选项类的发布质量实现。它们是可本地化的,并且编写为“发货质量”应用程序。

通用首选项文件

所有测试工具将共享 相同的首选项子类。这是一个相当简单的变体,具有以下数据类型

它为所有这些值呈现 键值可观察访问器,这些访问器直接在 macOS 测试工具中使用。

iOS 测试工具

iOS 测试工具应用程序是一个非常简单的单屏应用程序,它提供直接界面来编辑和查看通用首选项实例中的值。此外,它还提供了一个使用 Settings Bundle 在设置应用程序中显示“首选项窗格”的简单演示。我们只访问两个值,以便使演示尽可能基本,但有可能使其更复杂。

iOS 测试工具还集成了一个 watchOS 测试工具,该测试工具与设备应用程序共享首选项实例。

watchOS 测试工具

watchOS 测试工具实际上是 iOS 测试工具应用程序的一部分。它与 iOS 测试工具应用程序实例共享一个 RVS_PersistentPrefs 状态。

它是一个微型应用程序,仅演示将首选项传输到 Watch,并仅显示几个值(并响应于手机上的更改而更新它们)。它存在的主要原因是表明该类在 watchOS 中的工作方式与在 iOS 中的工作方式相同。您可以对 Watch 应用程序执行以影响数据的唯一操作是向手机发送重置命令。否则,它只是显示。

macOS 测试工具

macOS 测试工具应用程序对某些 UI 使用 KVO,因此某些用户输入字段和持久首选项之间存在“无代码”连接。

启动时,不会显示任何窗口。您需要进入应用程序菜单,然后选择“Preferences...”。这将显示窗口。

tvOS 测试工具

tvOS 测试工具显示与其他所有工具非常相似的布局,并允许演示该类在 tvOS 中的工作方式。