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

NSUbiquitousKeyValueStore
和 UserDefaults
功能基本一致,只是增加了基于 iCloud 的云同步功能,可以在多设备之前同步数据,即使用户卸载应用,数据仍然会保留。
NSUbiquitousKeyValueStore
非常适合用于存储用户偏好设置,以及一些微小且非敏感的数据。使用 NSUbiquitousKeyValueStore
时,总空间被限制为 1 MB。因此,不要用它来存储大量数据。
Apple 推荐使用场景

“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
同步支持:

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

设置 .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 访问
这是一个不错的扩展库:
@CloudStorage
让在 View 组件中使用 NSUbiquitousKeyValueStore
变得和 @AppStorage
一样简单方便。
但考虑到我当前使用 PreferencesManager 文件来统一管理 NSUbiquitousKeyValueStore
,因此暂时使用不上。
NSUbiquitousKeyValueStore 支持在 App 与小组件之间共享数据
这是个有启发的用例:
After 2 weeks grinding out learning Widgets, and how to correctly share data between the app, widget, and intents via NSPersistentCloudKitContainer + NSUbiquitousKeyValueStore....
— Michael Tigas (@michael_tigas) October 5, 2020
I've finally finished @FocusedWorkApp's widgets! pic.twitter.com/UFTIGLtFgQ
常见问题
云同步导致 App 崩溃
问题描述:
在 MONO 记账中,在一台设备上修改预算,在通过 NSUbiquitousKeyValueStore
同步数据时,会导致另一台设备 App 崩溃一次。
xcode 的错误提示为 Enqueued from com.apple.kvs.client (Thread 40)
,但没有更详细的错误日志,也没有定位到具体是哪行代码的问题。

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


1.首先,测试注释掉通知代码,崩溃问题消失。
说明崩溃和通知有关系:

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

进一步测试,确认是由于通知注册方法的问题。
更换了一种通知注册的方式 —— 使用 Combine 实现之后,问题暂时解决。
具体请参考 MONO 项目中 PreferencesManager
文件的实现。