PersistentKeyValueKit

Platform Versions Swift Versions Test

UserDefaultsNSUbiquitousKeyValueStore 提供完美接口。

关于

PersistentKeyValueKit 为 UserDefaultsNSUbiquitousKeyValueStore 提供了全面、类型安全和通用的接口。它可以轻松地在整个代码库中持久化和检索任何类型的数据。该框架鼓励:

PersistentKeyValueKit 包含了大量的意见、概念和类型,但其实现非常轻量级,并且试图直接位于熟悉的存储 API 之上。

UserDefaultsNSUbiquitousKeyValueStore 的所有约束都适用。建议您熟悉这些存储系统。数据不会在存储之间自动迁移。

PersistentKeyValueKit 由强大的测试套件支持。

功能

支持的平台

要求

文档

文档可在 GitHub Pages 上找到.

安装

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/kylehughes/PersistentKeyValueKit.git", .upToNextMajor(from: "1.0.0")),
]

快速开始

通过符合 KeyValuePersistible 协议使类型可持久化。

import PersistentKeyValueKit

enum RuntimeColorScheme: String {
    case dark
    case light
    case system
}

extension RuntimeColorScheme: KeyValuePersistible {
    static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> {
        RawRepresentablePersistentKeyValueRepresentation()
    }
}

定义一个键,其值为该类型。

import PersistentKeyValueKit

extension PersistentKeyProtocol where Self == PersistentKey<RuntimeColorScheme> {
    static var runtimeColorScheme: Self {
        Self("RuntimeColorScheme", defaultValue: .system)
    }
}

在 SwiftUI 视图中使用该值……

import PersistentKeyValueKit

struct SettingsView: View {
    @PersistentValue(.runtimeColorScheme)
    var runtimeColorScheme
}

……或任何其他地方。

userDefaults.get(.runtimeColorScheme)
userDefaults.set(.runtimeColorScheme, to: .dark)

用法

定义和访问键

PersistentKey<Value> 将唯一标识符映射到在应用程序启动之间持久存在的强类型 Value

建议使用静态访问器模式来定义和访问键。 此模式允许您在常用位置定义键,并在任何位置以类型安全的方式访问它们。 这些 API 旨在尽可能符合人体工程学地支持此模式。

例如

extension PersistentKeyProtocol where Self == PersistentKey<Date> {
    static var mostRecentLaunchDate: Self {
        Self("MostRecentLaunchDate", defaultValue: .distantPast)
    }
}

动态识别的键也可以使用静态访问器定义。

例如

extension PersistentKeyProtocol where Self == PersistentKey<String?> {
    static func selectedLayoutID(forListID listID: String) -> Self {
        Self("\(listID)::SelectedLayoutID", defaultValue: nil)
    }
}

键值对可以本地存储在 UserDefaults 中(例如 UserDefaults.standardUserDefaults(suiteName:))或存储在 iCloud 的 NSUbiquitousKeyValueStore.default 中。

例如

userDefaults.set(.mostRecentLaunchDate, to: .now)
if let layoutID = NSUbiquitousKeyValueStore.default.get(.selectedLayoutID(forListID: listID)) {

定义和访问调试键

调试键是一种键,其值可以在 Debug 构建中修改,但不能在 Release 构建中修改。这使您可以将键用于开发和测试目的,而不必担心它们在生产环境中被修改,同时最大限度地减少需要编写的条件代码量。

所有基于键的接口都接受 PersistentKeyProtocolPersistentKeyPersistentDebugKey 都符合该协议。

警告

调试键只有在从源代码编译此框架时(例如,作为 SwiftPM 依赖项)才能工作。如果使用预构建的二进制文件,则可能不包含 DEBUG 代码路径,并且将始终使用默认值。

例如

extension PersistentKeyProtocol where Self == PersistentDebugKey<Bool> {
    static var isAppStoreRatingEnabled: Self {
        Self(
            "IsAppStoreRatingEnabled", 
            debugDefaultValue: false, 
            releaseDefaultValue: true
        )
    }
}
userDefaults.set(.isAppStoreRatingEnabled, to: false)
userDefaults.get(.isAppStoreRatingEnabled) // false in Debug, true in Release

使类型可持久化

通过符合 KeyValuePersistible 协议使类型可持久化。

KeyValuePersistible 有一个要求

static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> { get }

表示形式是一种描述如何持久化值的类型:它如何存储在 UserDefaultsNSUbiquitousKeyValueStore 中,以及如何检索它。

提供了许多常见的表示形式,并且很容易在 KeyValuePersistible 实现内部构建自定义表示形式。存储的原始类型以原生方式表示,因此您的责任是将您的类型转换为原始类型。

例如

extension UIContentSizeCategory: KeyValuePersistible {
    static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> {
        RawRepresentablePersistentKeyValueRepresentation()
    }
}

原始类型

UserDefaults 和/或 NSUbiquitousKeyValueStore 本身支持原始类型。这些类型都是 KeyValuePersistible 并且直接存储,几乎不需要转换。所有其他的 KeyValuePersistible 类型都必须通过 PersistentKeyValueRepresentation 转换为原始类型。

原始类型包括:

持久化表示形式

有一些内置的表示形式,涵盖了应用程序最常见的用例。 它们都在这里描述。 如果需要,可以使用它们的构建块,但此处不描述。

ProxyPersistentKeyValueRepresentation

ProxyPersistentKeyValueRepresentation 是一种使用 Proxy 的表示形式作为自身的表示形式。 Proxy 类型必须是 KeyValuePersistible 类型。

使用此表示形式来依赖于适合该类型的现有表示形式。

这是类型在其之上构建的基本表示形式。 一种常见的模式是使用原始类型作为代理类型,但是任何 KeyValuePersistible 类型都可以用作代理类型。 间接层数没有限制。

例如

Date 持久化为 TimeInterval(即 Double)。

extension Date: KeyValuePersistible {
    public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> {
        ProxyPersistentKeyValueRepresentation(
            to: \.timeIntervalSinceReferenceDate,
            from: Date.init(timeIntervalSinceReferenceDate:)
        )
    }
}

RawRepresentablePersistentKeyValueRepresentation

RawRepresentablePersistentKeyValueRepresentation 是一种代理表示形式,如果 RawValueKeyValuePersistible,则将 RawRepresentable 值持久化为 RawValue

例如

NotificationFrequency 持久化为 String

enum NotificationFrequency: String {
    case daily
    case weekly
    case monthly
}
extension NotificationFrequency: KeyValuePersistible {
    static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> {
        RawRepresentablePersistentKeyValueRepresentation()
    }
}

CodablePersistentKeyValueRepresentation

CodablePersistentKeyValueRepresentation 是一种代理表示形式,它将值持久化为给定编码器和解码器的 Input/Output 类型。

例如

Contact 持久化为 Data

struct Contact: Codable, Sendable {
    let nickname: String
    let dateOfBirth: Date
}
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
extension Contact: KeyValuePersistible {
    static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> {
        CodablePersistentKeyValueRepresentation()
    }
}

为此默认 JSONDecoderJSONEncoder 提供了此便捷初始化程序。 可以在其他初始化程序中提供 DecoderEncoder

LosslessStringConvertiblePersistentKeyValueRepresentation

LosslessStringConvertiblePersistentKeyValueRepresentation 是一种代理表示形式,它将值持久化为 String,如其 LosslessStringConvertible 一致性所定义。

例如

Character 持久化为 String

extension Character: KeyValuePersistible {
    static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation<Self> {
        LosslessStringConvertiblePersistentKeyValueRepresentation()
    }
}

单个键的自定义表示

每个 KeyValuePersistible 类型都有一个关联的 PersistentKeyValueRepresentation 类型和 persistentKeyValueRepresentation 属性。 这是用于使用存储持久化值的默认表示形式。

但是,可以在定义时为单个键提供表示形式,即使该值不是 KeyValuePersistible。 这对于以下情况很有用:

例如

Date? 使用 ISO 8601 格式持久化为 String

extension PersistentKeyProtocol where Self == PersistentKey<Date?> {
    static var mostRecentAppStoreReviewRequestDate: Self {
        Self(
            "MostRecentAppStoreReviewRequestDate",
            defaultValue: nil,
            representation: ProxyPersistentKeyValueRepresentation(
                to: { date in date.ISO8601Format() },
                from: { string in try? Date.ISO8601FormatStyle().parse(string) }
            )
        )
    }
}

SwiftUI

属性包装器

PersistentValue 是一个属性包装器,它提供了一种类型安全的方式来访问和修改 SwiftUI 视图中 UserDefaultsNSUbiquitousKeyValueStore 中的值。 它支持自动观察和更新,无论何时给定存储中的值发生更改(本地或其他方式)。

默认存储是来自环境的 defaultPersistentKeyValueStore。 如果未设置,则默认存储为 UserDefaults.standard

例如

@PersistentValue(.isAppStoreRatingEnabled)
var isAppStoreRatingEnabled: Bool
@PersistentValue(.isAppStoreRatingEnabled, store: .ubiquitous)
var isAppStoreRatingEnabled: Bool
视图修饰符

提供了一个视图修饰符,用于设置视图(或其后代)中任何 @PersistentValue 属性包装器使用的默认存储。 可以通过直接在 @PersistentValue 声明中提供默认存储来覆盖默认存储。

例如

extension App: SwiftUI.App {
    var body: some Scene {
        RootView()
            .defaultPersistentKeyValueStore(.ubiquitous)
    }
}

UserDefaults 注册

PersistentKeyValueKit 支持传统的 UserDefaults 注册。 键的默认值将注册为 UserDefaults 实例注册域中的默认值。

例如

1 将为 UserDefaults 注册域中的默认值字典中的键 LaunchCount 注册。

extension PersistentKeyProtocol where Self == PersistentKey<Int> {
    static var launchCount: Self {
        Self("LaunchCount", defaultValue: 1)
    }
}
userDefaults.register(.launchCount)

注意

仅使用 PersistentKeyValueKit 时,注册不是必需的,因为默认值通过键定义来处理。 当与其他不使用 PersistentKeyValueKit 的框架共享 UserDefaults 时,它变得有用,确保使用原始 UserDefaults API 的代码可以使用默认值。

重要的行为差异

PersistentKeyValueKit 致力于成为平台存储 API 之上的类型安全基础设施。 只有当它具有压倒性的惯用性、现代性或必要性时,才会更改行为。

没有隐式默认值

平台存储 API 使用隐式默认值。 例如,UserDefaults 将为未设置(或已删除)的键的 Bool 值返回 false,或者为 Int 返回 0。 无法将这些隐式值与未设置的键区分开来; 无法表示 nil 值或值的缺失。

PersistentKeyValueKit 要求每个键都有一个显式默认值。 这是将为未设置的键或具有错误类型值的键返回的值。 每个键都可以不同。 这提供了对值的精细控制以及类型安全的可选性。 如果该键需要表示 nil 值或值的缺失,则可以使用 Optional 类型定义该键,并且其默认值可以为 nil

例如

此键的值可以为 nil,如果未设置任何值,则将返回 nil。 调用者可以将未设置的键与 truefalse 值区分开来。

extension PersistentKeyProtocol where Self == PersistentKey<Bool?> {
    static var arePushNotificationsEnabled: Self {
        Self("ArePushNotificationsEnabled", defaultValue: nil)
    }
}

此键的值不能为 nil,如果未设置任何值,则将返回 true。 调用者无法将未设置的键与 true 值区分开来。

extension PersistentKeyProtocol where Self == PersistentKey<Bool> {
    static var arePushNotificationsEnabled: Self {
        Self("ArePushNotificationsEnabled", defaultValue: true)
    }
}

注意

对于 boolean 值缺失的含义的绝对清晰,具有三种情况的枚举通常比 Optional<Bool> 更好。

没有异构集合

平台存储 API 支持异构数组 (Array<Any>) 和字典 (Dictionary<String, Any>)。

出于人体工程学考虑,PersistentKeyValueKit 本身不支持异构集合。 不允许重叠的协议一致性(即对于 KeyValuePersistible),因此决定支持 Swift 中更常用的同构集合。

KeyValuePersistible KeyValuePersistible
[Element],其中 Element: KeyValuePersistible [any KeyValuePersistible]
[String: Value],其中 Value: KeyValuePersistible [String: any KeyValuePersistible]

异构数组难以在 Swift 中使用:调用者需要知道每个索引处的类型。 异构字典是可以理解的(序列化键控类型),但在框架内正确支持它们并没有提供优于使用 Codable 表示的性能优势。

通过使类型符合 PrimitiveKeyValuePersistible 协议并直接与存储 API 交互,可以使用异构集合。 这是持久化键值类型的最快方法。 但是,不建议这样做,因为没有针对属性列表安全或代理表示的支持,但它是可用的。

NSUbiquitousKeyValueStore 的有限可观察性

平台不支持观察 NSUbiquitousKeyValueStore 中键的更改。 唯一的方法是监听来自其他设备的外部更改。 PersistentKeyValueKit 实现了通过框架进行的所有修改的可观察性: 任何使用 NSUbiquitousKeyValueStore@PersistentValue 将自动更新 PersistentKeyValueKit 在任何地方、任何设备上所做的任何更改。 但是,在框架之外对 NSUbiquitousKeyValueStore 所做的任何更改将不会自动反映在 @PersistentValue 属性中。

贡献

PersistentKeyValueKit 目前不接受源代码贡献。 将考虑提交 Bug 报告。

作者

Kyle Hughes

Bluesky
LinkedIn
Mastodon

资源

许可

PersistentKeyValueKit 在 MIT 许可下可用。

详情请参阅 LICENSE 文件。