Fancy logo Fancy logo

TinyStorage

一个简单、轻量级的 UserDefaults 替代方案,具有更可靠的访问和对 Codable 类型的原生支持。

概述

源于遇到 UserDefaults 的问题。正如那篇博客文章所讨论的,由于设备被锁定并且 iOS “预启动” 你的应用,UserDefaults 越来越多地出现返回 nil 数据的问题,这让我无法真正信任 UserDefaults 返回的内容。再加上一个没有很好地呈现这些信息的 API,你很容易发现自己陷入难以追踪的 bug 和数据丢失的境地。该库旨在从根本上解决这个问题,它不加密支持文件,从而能够更可靠地访问你保存的数据(但安全性较低,因此不要存储敏感数据),并在此基础上添加了一些便利功能。

这意味着它非常适合存储偏好设置和数据集合,例如用户喜欢的鸟类物种,但不适合存储敏感信息。请勿存储密码/密钥/令牌/秘密/日记条目/奶奶的意大利面食谱,以及任何可以被认为是敏感用户信息的物品,因为它们没有在磁盘上加密。但是,也请不要使用 UserDefaults 存储敏感信息,因为只要用户在重启后解锁过设备,UserDefaults 数据在设备锁定时仍然会被完全解密。而应该使用 Keychain 来存储敏感数据。

UserDefaults 一样,TinyStorage 旨在用于相对较小的值。不要在 TinyStorage 中存储大型数据库,因为它没有针对此进行优化,但对于检索存储的 Codable 类型来说,速度足够快。作为参考,我认为保持在 1 MB 以下。

这种可靠地存储小型、非敏感数据(对我而言)一直是 UserDefaults 应该擅长的事情,因此该库试图实现这一愿景。它非常简单,只有几百行代码,远非文件系统工程的奇迹,但只是一个希望有用的小工具!

(另外要明确的是,TinyStorage 不是 UserDefaults 的封装器,它是一个完整的替代方案。它不以任何方式与 UserDefaults 系统交互。)

🧪 实验性/Beta 版

TinyStorage 仍在不断变化/积极开发中,因此 API 可能会发生变化,并且肯定存在出现 bug 的可能性。我主要想尽早将其发布到世上,以防有人觉得它有趣,但如果你不希望它进行大量更改,请考虑 Fork 它或在 Swift Package Manager 中固定特定版本。也非常欢迎反馈/PR!

特性

限制

UserDefaults 不同,TinyStorage 不支持混合集合,因此如果你在 UserDefaults 中有一个包含字符串、日期和整数的数组,而没有将它们装箱到一个共享类型中,那么 TinyStorage 将无法正常工作。字典也是如此,你可以很好地将它们与 TinyStorage 一起使用,但键和值都必须是 Codable 类型,因此你不能使用 [String: Any],因为每个字符串键可能包含不同类型的值。

安装

只需为 https://github.com/christianselig/TinyStorage.git 添加一个 Swift Package Manager 依赖项

由于使用了较新的 Observation 框架,TinyStorage 需要 iOS 17 或更高版本。如果你必须支持旧版本的 iOS,@newky2k 已经友善地 Fork 了一个使用较旧的 @ObservableObject 系统的版本!https://github.com/newky2k/TinyStorage

用法

首先,初始化一个 TinyStorage 实例或创建一个单例,首先选择你想将磁盘上的文件保存在哪里,其次选择将要创建的目录的名称以容纳支持 plist 文件(如果你想创建多个 TinyStorage 实例,只需为每个实例指定不同的 name 即可!)。为了与 UserDefaults 约定保持一致,我通常为应用程序容器创建一个单例

extension TinyStorage {
    static let appGroup: TinyStorage = {
        let appGroupID = "group.com.christianselig.example"
        let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)!
        return .init(insideDirectory: containerURL, name: "tiny-storage-general-prefs")
    }()
}

(不过,你可以将它存储在你认为合适的任何地方,例如 URL.documentsDirectory 也是一个想法!)

然后,决定你想要如何引用你的键,类似于 UserDefaults,你可以使用原始字符串,但我建议使用更强类型的方法,只需让一个类型符合 TinyStorageKey 并返回一个 var rawValue: String,然后你可以将它用作存储的键,而无需担心拼写错误。如果你正在使用类似 enum 的东西,将其设为 String 枚举可以免费为你提供此功能,因此无需额外工作!

之后,你可以简单地在 TinyStorge 实例中读/写值

enum AppStorageKeys: String, TinyStorageKey {
    case likesIceCream
    case pet
    case hasBeatFirstLevel
}

// Read
let pet: Pet? = TinyStorage.appGroup.retrieve(type: Pet.self, forKey: AppStorageKeys.pet)

// Write
TinyStorage.appGroup.store(true, forKey: AppStorageKeys.likesIceCream)

(如果你有一些非常奇怪的类型或者不想符合 Codable,只需通过你喜欢的任何方式将该类型转换为 Data 并存储,因为 Data 本身就是 Codable。)

如果存储中不存在某个值,尝试检索它将始终返回 nil。这与 UserDefaults 形成对比,在 UserDefaults 中,一些原始类型(如 Int 或 Bool)在不存在时将分别返回 0 或 false,而不是 nil

如果你想在 SwiftUI 中使用它,并让你的视图自动响应存储中项目的更改,你可以使用 @TinyStorageItem 属性包装器。只需指定你的存储,你要访问的项目的键,并指定一个默认值。

@TinyStorageItem(AppStorageKey.pet, storage: .appGroup)
var pet = Pet(name: "Boots", species: .fish, hasLegs: false)

var body: some View {
    Text(pet.name)
}

你甚至可以使用 Bindings 来自动读取/写入。

@TinyStorageItem(AppStorageKeys.message, storage: .appGroup)
var message: String = ""

var body: some View {
    VStack {
        Text("Stored Value: \(message)")
        TextField("Message", text: $message)
    }
}

它还解决了 @AppStorage 的一些烦恼,例如无法存储集合

@TinyStorageItem("names", storage: .appGroup)
var names: [String] = []

或者更好地支持可选值

@TinyStorageItem("nickname", storage: .appGroup)
var nickname: String? = nil // or "Cool Guy"

你还可以使用一个方便的辅助函数从 UserDefaults 实例迁移到 TinyStorage

let nonBoolKeysToMigrate = ["favoriteIceCream", "appFontSize", "lastFetchDate"]
let boolKeysToMigrate = ["hasEatenIceCreamRecently", "usesCustomTheme"]

TinyStorage.appGroup.migrate(userDefaults: .standard, nonBoolKeys: nonBoolKeysToMigrate, boolKeys: boolKeysToMigrate, overwriteIfConflict: true)

请注意,你必须指定哪些键对应于布尔值,哪些键不对应,因为 UserDefaults 本身只是在后台将布尔值存储为整数,并且作为迁移的一部分,我们希望将布尔值存储为正确的 Swift Bool 类型,并且我们 TinyStorage 无法知道 UserDefaults 中的 1 应该表示 true 还是实际的 1,除非你输入。阅读 migrate 函数文档以了解其他重要详细信息!

如果你想手动迁移多个键或一次存储大量内容,而不是一堆单独的 store 调用,你可以使用 bulkStore 将它们合并到一个调用中,该调用只会写入磁盘一次

TinyStorage.appGroup.bulkStore(items: [
    AppStorageKeys.pet: pet,
    AppStorageKeys.theme: "sunset"
], skipKeyIfAlreadyPresent: false)

(当设置为 true 时,skipKeyIfAlreadyPresent 会创建一个类似于 UserDefaults 中的 registerDefaults 的 API。)

祝你存储愉快,希望你喜欢!💾