SwiftData|为 App 添加预设数据

想象一下,一个代办事项 App 需要提供标签功能,并且你想为 App 提供一些预设的标签供用户选择。

  • 使用枚举来实现标签功能。这会导致用户无法创建自定义标签,因此枚举是写死的。
  • 使用 SwiftData 实现标签功能 —— 这时候就需要为 SwiftData 生成预设数据。

SwiftData 创建预设数据的挑战

一种最简单的方法是,使用枚举来定义预设数据,并在 App 启动时检查是否有预设数据,如果没有就自动创建并添加 SwiftData。

这看起来很简单,但在实践中仍然存在一些困难。

CloudKit 导致的数据重复

如果启用了 CloudKit 云同步,当用户在同一个 Apple 账户的另一台设备安装,或者同一台设备卸载后再次安装时,由于 iCloud 数据同步存在延迟,会导致初始化程序创建重复的 SwiftData 数据。

因此,我们不能够简单的仅通过检查本地是否有数据来决定是否添加预设数据。

解决方案:需要同时检查 CloudKit 云端,仅在本地和云端均无数据时,才执行添加操作。检查本地是因为在首次安装时,本地数据可能尚未上传到 iCloud。

How to preload CloudKit SwiftData with data on first launch – SwiftUI – Hacking with Swift forums
SwiftUI – Hacking with Swift forums

该帖子详细讨论了这个问题

💡
搜索 SwiftData 与该问题,能找到的资料有限。但尝试搜索 Core Data 中 initial data "CloudKit" duplicate 问题,能找到大量的讨论。

执行层面的一些关键点:

  • 创建 /PresetData/CloudKitPresetService 专门用于执行 CloutKit 数据查询
  • 在 SwiftData 模型中,添加可选的 presetID 参数。在枚举中也添加 presetID 属性。如果 presetID 不为 nil,表示是预设数据。

枚举更新后如何同步到 SwiftData

  1. 首先,通过 presetID 区分哪些数据是预设,哪些是自定义。
  2. 对于预设数据,不在 SwiftData 中存储字段数据,而是直接调用枚举中的数据。可以参考 HiFit 中 EquipmentEntry 的实现。

不可行的解决方案

将预设数据存储在单独的容器中。

在下面这个论坛中,@delawaremathguy 提到一种思路,将预设数据和用户创建数据区分为不同的容器,只应当将用户创建的数据同步到 CloudKit 中。

这个理论上很棒,但在实践中,由于模型的不一致,会导致增加大量的格外工作。有点得不偿失。

https://stackoverflow.com/questions/78269948/prepopulate-container-without-duplication-when-using-swiftdata-and-cloudkit

让用户主动创建示例数据,而不是自动创建。

例如,如果本地不存在数据时,弹窗提醒用户是否创建预设数据。

但这种方法任然可能存在的情况是,CloudKit 数据还未同步完成,用户点击导致了再次创建,仍然会导致数据重复。并不是一个很棒的解决方案。

在 CloudKit 中创建「标志符」,控制初始化程序

使用「标识符」进行判断,而不是「是否存在某条数据」

在 CloudKit 数据同步完成后,不应当通过“是否存在某条数据”来判断是否需要初始化预设数据。

原因在于,即使第一台设备已经执行过初始化,用户仍可能已经手动删除或修改了这些数据。相反,我们只需要一个「标志符」来判断,该用户在所有设备上,是否为首次安装运行 App,从而决定是否执行初始化流程。

有两种解决方案:

方案一:通过注册账号来判断。

例如,对于 SignInWithApple 选项,用户仅在第一次登录时提供 username,我们可以通过这个来判断用户是否初次安装。

但缺点是,必须强制启用用户登录,这不符合最佳实践。

方案二:在 CloudKit 中创建一个标志符

例如,可以创建一个 AppConfiguration 模型,并通过不同的变量,控制不同 Model 的初始化:

更方便的是,实际上我们无需在 App 中注册这个模型,可以直接在 CloudKit 云端操作即可,下面会详细说明这一点。

为什么不适合使用 NSUbiquitousKeyValueStore ?

NSUbiquitousKeyValueStore 很适合存储「标志符」类型,因为它是轻量级的。但 NSUbiquitousKeyValueStore 未提供主动的查询方法。

虽然可以调用synchronize() 方法来请求同步,但你不知道什么时候同步完成,是否为云端的最新值,所以难以使用它来决定是否执行初始化程序。

通过 CloudKit 查询或更新「标志符」

在 AppConfiguration 扩展中创建一个状态查询方法,以及一个值更新方法。

直接通过 CloudKit 提供的 CKQueryCKRecord 等接口,直接操作云端数据。

在模型扩展中,创建初始化方法

如果只需云端存储,是否有必要在本地创建和注册 AppConfiguration 模型?

我尝试从 App 中删除 AppConfiguration 的注册,并从 CloudKit Dashboard 中删除该表之后,查询出现了短暂的 type busy 错误:

经过几个小时都未恢复。最后,我尝试重置 Environment 环境后,该问题才解决。

然后,我测试删除 AppConfiguration 模型,测试结果显示仍然可以工作。

无可避免的重复数据创建之后,如何清除?