一个简单、轻量级的 UserDefaults
替代方案,具有更可靠的访问和对 Codable
类型的原生支持。
源于遇到 UserDefaults
的问题。正如那篇博客文章所讨论的,由于设备被锁定并且 iOS “预启动” 你的应用,UserDefaults
越来越多地出现返回 nil 数据的问题,这让我无法真正信任 UserDefaults
返回的内容。再加上一个没有很好地呈现这些信息的 API,你很容易发现自己陷入难以追踪的 bug 和数据丢失的境地。该库旨在从根本上解决这个问题,它不加密支持文件,从而能够更可靠地访问你保存的数据(但安全性较低,因此不要存储敏感数据),并在此基础上添加了一些便利功能。
这意味着它非常适合存储偏好设置和数据集合,例如用户喜欢的鸟类物种,但不适合存储敏感信息。请勿存储密码/密钥/令牌/秘密/日记条目/奶奶的意大利面食谱,以及任何可以被认为是敏感用户信息的物品,因为它们没有在磁盘上加密。但是,也请不要使用 UserDefaults
存储敏感信息,因为只要用户在重启后解锁过设备,UserDefaults
数据在设备锁定时仍然会被完全解密。而应该使用 Keychain
来存储敏感数据。
与 UserDefaults
一样,TinyStorage
旨在用于相对较小的值。不要在 TinyStorage
中存储大型数据库,因为它没有针对此进行优化,但对于检索存储的 Codable
类型来说,速度足够快。作为参考,我认为保持在 1 MB 以下。
这种可靠地存储小型、非敏感数据(对我而言)一直是 UserDefaults
应该擅长的事情,因此该库试图实现这一愿景。它非常简单,只有几百行代码,远非文件系统工程的奇迹,但只是一个希望有用的小工具!
(另外要明确的是,TinyStorage
不是 UserDefaults
的封装器,它是一个完整的替代方案。它不以任何方式与 UserDefaults
系统交互。)
TinyStorage 仍在不断变化/积极开发中,因此 API 可能会发生变化,并且肯定存在出现 bug 的可能性。我主要想尽早将其发布到世上,以防有人觉得它有趣,但如果你不希望它进行大量更改,请考虑 Fork 它或在 Swift Package Manager 中固定特定版本。也非常欢迎反馈/PR!
TinyStorage
也能正确读取和写入数据Codable
类型UserDefaults
类似,使用磁盘存储之上的内存缓存来提高性能DispatchQueue
实现线程安全,因此你可以安全地跨线程读取/写入,而无需自己协调NSFileCoordinator
来协调磁盘的读取/写入,因此可以同时在多个进程中使用(例如,主目标和 Widget 目标)@AppStorage
)NotificationCenter
中订阅 TinyStorage.didChangeNotification
OSLog
进行日志记录UserDefaults
实例迁移到 TinyStorage
的函数与 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。)
祝你存储愉快,希望你喜欢!💾