
假设您需要将用户ID保存在UserDefaults中 。 第一步是什么?
通常,企业从为密钥添加常量并检查其唯一性开始。 对于大多数其他键值存储区来说,这是正确的。 此类存储库的原始设计的结果不仅限于密钥;以非系统方法集的形式出现的接口会导致许多可能的问题:
- 编写键时出错 :可以使用不同的键来读取和写入同一实体。
- 值的不固定类型 :例如,使用相同的键,您可以写入数字和读取字符串。
- 按键冲突 :具有相同按键的不同实体可以记录在项目的不同部分。
在理想情况下,编译器应避免出现这些问题,如果存在键冲突或值类型不匹配,则不允许您构建项目。 为了实现这种安全存储,可以使用容器作为值,本演练将帮助您使用UserDefaults
作为示例来准备它们。
存储协议
因此,我们首先需要的是存储库本身的协议,这将有助于从存储库类型中抽象出来。 因此,我们将有一个单一接口,可与UserDefaults
,钥匙串以及其他一些键值存储一起使用。 该协议看起来非常简单:
protocol KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? func setValue<T: Codable>(_ value: T?, forKey key: String) }
因此,任何符合KeyValueStorage
协议的存储都必须实现两个通用方法 :字符串形式的键值的getter和setter。 同时,值本身对应于Codable
协议,该协议允许存储具有通用表示形式的类型的实例(例如JSON或PropertyList )。
标准数据仓库实现不支持Codable
类型的值,例如,相同的UserDefaults
。 因此,这种类型上的区别是一个附加好处,它使您既可以存储Swift原语(数字,字符串等)又可以存储整个数据结构,同时保持存储接口的简单性。
协议实施
有两种方法可以实现KeyValueStorage
协议:
extension UserDefaults: KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? {
class PersistentStorage: KeyValueStorage { private let userDefaults: UserDefaults let suiteName: String? let keyPrefix: String init?(suiteName: String? = nil, keyPrefix: String = "") { guard let userDefaults = UserDefaults(suiteName: suiteName) else { return nil } self.userDefaults = userDefaults self.suiteName = suiteName self.keyPrefix = keyPrefix } func value<T: Codable>(forKey key: String) -> T? {
第二种方法看起来更复杂,但是扩展性更好,例如,您可以为每个存储实例设置键前缀。 另外,包装器隐藏额外的字段,仅保留协议字段可用,从而解决了签名冲突的潜在问题。
要自己实现方法value(forKey:)
和setValue(:forKey:)
提供数据兼容性非常重要。 这是必需的,以便可以通过KeyValueStorage
的方法检索标准UserDefaults
工具存储的值,反之亦然。
可在此处使用现成的PersistentStorage
类的完整示例。
价值容器
现在,我们已经从存储类型中抽象出来了,我们将通过键为值添加一个容器。 将所有必需字段封装到一个方便的实体中将很有用,该实体可以与存储本身分开传输和使用。 这样的容器被实现为一个小的通用类 :
class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String var value: T? { get { storage.value(forKey: key) } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String) { self.storage = storage self.key = key } }
容器的值类型受Codable
协议限制的方式与存储本身的方法相同,因此,计算出的属性value
仅将具有固定键和值类型的调用代理给它。
如果需要,可以扩展容器的功能,例如,添加默认值,如果存储中没有密钥的数据,则将使用该默认值。
具有默认值的容器的示例实现 class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String let defaultValue: T? var value: T? { get { storage.value(forKey: key) ?? defaultValue } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String, defaultValue: T? = nil) { self.storage = storage self.key = key self.defaultValue = defaultValue } }
容器解决了我们的前两个问题-它为读取和写入的数据确定了密钥和类型。 因此,任何试图写入错误类型值的尝试都将在编译阶段由编译器停止:
func doSomething(with container: KeyValueContainer<Int>) { container.value = "Text"
解决最后一个问题仍然是要保证存储密钥的唯一性。 也可以使用相当有趣的技巧将这种担忧挂在编译器上。
关键唯一性
为了解决冲突问题,我们使用将在其中创建容器本身的计算属性的名称作为键。 为此,向我们的KeyValueStorage
协议添加一个简单的扩展:
extension KeyValueStorage { func makeContainer<T: Codable>(key: String = #function) -> KeyValueContainer<T> { KeyValueContainer(storage: self, key: key) } }
因此,在存储协议的所有实现中,将添加通用方法,该方法返回具有指定键的容器。 此方法特别key
是key
参数,默认情况下它的值等于特殊表达式#function
( documentation )。 这意味着在构建阶段,将#function
文字替换为makeContainer(key:)
调用makeContainer(key:)
方法的声明的名称。
这种构造允许您在存储扩展中声明容器,并且如果makeContainer()
方法而不包含key
参数,则它们的名称将是计算属性的名称:
extension PersistentStorage { var foobar: KeyValueContainer<Int> { makeContainer() } }
在该示例中, PersistentStorage
存储实例将使用具有相同名称的foobar
键的容器接收foobar
属性,其值类型将为整数。 尝试添加第二个foobar
容器进行存储将导致编译错误,从而保证了键的唯一性。

总结一下
值容器解决了所有提到的存储接口问题,并且不限于UserDefaults
。 在具有相应实现的KeyValueStorage
协议下签名(包装)任何键值存储就足够了,并且它已经可以用于创建安全容器。
另外,值得注意的是添加和使用此类容器的便利性,所有内容都限于简单易懂的设计:
extension PersistentStorage {
您只需要习惯一个事实,即计算属性的名称是该值的键,这在重构代码时尤其重要。 另外,如果您仍然无法重命名,请不要忘记存储库的迁移。
仅此而已。 我很乐意在评论中提供反馈。 再见!