Foundation|使用 NSUbiquitousKeyValueStore 同步轻量数据

了解如何使用 NSUbiquitousKeyValueStore 替代 UserDefaults 存储轻量数据,并在多设备端同步。

Foundation|使用 NSUbiquitousKeyValueStore 同步轻量数据

NSUbiquitousKeyValueStoreUserDefaults 功能基本一致,只是增加了基于 iCloud 的云同步功能,可以在多设备之前同步数据,即使用户卸载应用,数据仍然会保留。

NSUbiquitousKeyValueStore非常适合用于存储用户偏好设置,以及一些微小且非敏感的数据。使用 NSUbiquitousKeyValueStore 时,总空间被限制为 1 MB。因此,不要用它来存储大量数据。

Apple 推荐使用场景

https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/iCloudFundametals.html#//apple_ref/doc/uid/TP40012094-CH6-SW28
“Every app submitted to the App Store or Mac App Store should take advantage of key-value storage.”

Apple:每一个提交到 App Store 或 Mac App Store 的应用,都建议充分利用 iCloud 的 key-value 存储功能来改善用户体验

为项目启动 NSUbiquitousKeyValueStore 同步功能

和启用 CloudKit 云同步类似,我们需要手动为项目添加 Key-value storage 同步支持:

如果不清楚如何添加,可参考:

SwiftData 教程与示例:添加 CloudKit 云同步(六)
了解如何为你的 iOS 应用添加 CloudKit 集成,并使用 iCloud 自动云同步你的应用数据。

设置 .entitlements 文件

为项目添加Key-value storage 同步之后,在 .entitlements 文件中(不是 info.plist),Xcode 会自动添加 iCloud Key-Value Store 键值对:

一般情况在,无需编辑 .entitlements 文件进行格外的设置。

如果你需要跨项目共享 Key-value storage,则这里需要格外的配置,此处暂不展开。

NSUbiquitousKeyValueStore 基本用法

基本增删改查方法

通常可以通过 NSUbiquitousKeyValueStore 提供的 default 单例对象来操作数据。

写入数据


let store = NSUbiquitousKeyValueStore.default

store.set(true, forKey: "hasSeenOnboarding")
store.set("dark", forKey: "selectedTheme")
store.set(42, forKey: "userScore")

支持的类型:String, Int, Double, Bool, Date, Data, Array, Dictionary

读取数据

let hasSeen = store.bool(forKey: "hasSeenOnboarding")
let theme = store.string(forKey: "selectedTheme") ?? "light"
let score = store.longLong(forKey: "userScore")

删除操作

store.removeObject(forKey: "hasSeenOnboarding")

为 NSUbiquitousKeyValueStore 提供默认值

NSUbiquitousKeyValueStore 不直接支持像 UserDefaults 那样直接设置默认值。

但是,我们可以通过运算符提供默认值:

let store = NSUbiquitousKeyValueStore.default

let theme = store.string(forKey: "selectedTheme") ?? "light"
let hasSeen = store.object(forKey: "hasSeenOnboarding") as? Bool ?? false

需要使用异步加载?

读取操作NSUbiquitousKeyValueStore.default.bool(forKey:)访问的是本地缓存,几乎是瞬时的,因此通常不需要特意实现异步加载。

使用需要手动调用更新?

创建一个 ViewModel 文件来统一管理变量

创建一个 ViewModel 或 Manager 文件,来统一管理 NSUbiquitousKeyValueStore 变量,会让维护变得更加简单。

例如,创建一个 PreferencesManager 文件,在这个文件中可以更加统一的处理日志打印、默认值、设置通知监听等:

使用 @CloudStorage 简化 NSUbiquitousKeyValueStore 访问

这是一个不错的扩展库:

GitHub - nonstrict-hq/CloudStorage: Swift property wrapper to sync settings through iCloud key-value storage
Swift property wrapper to sync settings through iCloud key-value storage - nonstrict-hq/CloudStorage

@CloudStorage 让在 View 组件中使用 NSUbiquitousKeyValueStore 变得和 @AppStorage 一样简单方便。

但考虑到我当前使用 PreferencesManager 文件来统一管理 NSUbiquitousKeyValueStore,因此暂时使用不上。

NSUbiquitousKeyValueStore 支持在 App 与小组件之间共享数据

这是个有启发的用例:

常见问题

云同步导致 App 崩溃

问题描述:

在 MONO 记账中,在一台设备上修改预算,在通过 NSUbiquitousKeyValueStore 同步数据时,会导致另一台设备 App 崩溃一次。

xcode 的错误提示为 Enqueued from com.apple.kvs.client (Thread 40),但没有更详细的错误日志,也没有定位到具体是哪行代码的问题。

在初始化的时候,注册通知:

1.首先,测试注释掉通知代码,崩溃问题消失。

说明崩溃和通知有关系

2.保留通知注册代码,但注释掉 cloudStoreDidChange() 方法的代码,仍然会崩溃

进一步测试,确认是由于通知注册方法的问题。

更换了一种通知注册的方式 —— 使用 Combine 实现之后,问题暂时解决。

具体请参考 MONO 项目中 PreferencesManager 文件的实现。