NSPersistentCloudKitContainer:如何检查数据是否同步到 CloudKit

FE_*_*ech 19 core-data cloudkit ios13 nspersistentcloudkitcontainer

我已实现NSPersistentCloudKitContainer将我的数据同步到CloudKit,我想知道同步已完成并且没有其他待同步的更改。

当我尝试重新安装该应用程序时,我开始从 CloudKit 取回我的数据并开始在控制台中打印某些日志。从 CloudKit 取回所有数据大约需要 30 秒。一些日志提到了NSCloudKitMirroringDelegate. 看起来NSCloudKitMirroringDelegate知道剩余的同步请求,但我找不到任何关于确保同步完成的信息。

这里有一些日志确实表明 NSCloudKitMirroringDelegate 知道同步何时完成。

CoreData: CloudKit: CoreData+CloudKit: -NSCloudKitMirroringDelegate checkAndExecuteNextRequest::检查待处理的请求。

CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _enqueueRequest:]_block_invoke(714): : 入队请求: A2BB21B3-BD1B-4500-865C-6C848D67081D

CoreData:CloudKit:CoreData+CloudKit:-[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2085)::推迟额外的工作。还有一个活动请求:A3E1D4A4-2BDE-4E6A-8DB4-54C96BA0579E

CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2092): :没有更多的请求要执行。

有什么办法可以知道数据完全同步了吗?我需要向用户显示某些 UI。

ggr*_*uen 20

引用 Apple Developer 论坛中类似问题中的“框架工程师”:“这是一个谬论”。在分布式系统中,您无法真正知道“同步是否完成”,因为此时可能在线或离线的另一台设备可能有未同步的更改。

也就是说,您可以使用以下一些技术来实现倾向于了解同步状态的用例。

添加默认/样本数据

给他们一个按钮来添加特定的默认/示例数据,而不是自动将其添加到应用程序中。这在分布式环境中效果更好,并使您的应用程序的功能和示例数据之间的区别更加清晰。

例如,在我的一个应用程序中,用户可以创建一个“上下文”列表(例如“家庭”、“工作”),他们可以在其中添加要执行的操作。如果用户是第一次使用该应用程序,“上下文”列表将为空。这很好,因为他们可以添加上下文,但提供一些默认值会很好。

我没有检测首次启动并添加默认上下文,而是添加了一个按钮,该按钮仅在数据库中没有上下文时才会出现。也就是说,如果用户导航到“Next Actions”屏幕,并且没有上下文(即contexts.isEmpty),则该屏幕还包含“添加默认 GTD 上下文”按钮。添加上下文的那一刻(由用户或通过同步),按钮消失。

下一步操作屏幕的屏幕截图

在此处输入图片说明

这是屏幕的 SwiftUI 代码:

import SwiftUI

/// The user's list of contexts, plus an add button
struct NextActionsLists: View {

    /// The Core Data enviroment in which we should perform operations
    @Environment(\.managedObjectContext) var managedObjectContext

    /// The available list of GTD contexts to which an action can be assigned, sorted alphabetically
    @FetchRequest(sortDescriptors: [
        NSSortDescriptor(key: "name", ascending: true)]) var contexts: FetchedResults<ContextMO>

    var body: some View {
        Group {
            // User-created lists
            ForEach(contexts) { context in
                NavigationLink(
                    destination: ContextListActionListView(context: context),
                    label: { ContextListCellView(context: context) }
                ).isDetailLink(false)
                    .accessibility(identifier: "\(context.name)") // So we can find it without the count
            }
            .onDelete(perform: delete)

            ContextAddButtonView(displayComplicationWarning: contexts.count > 8)

            if contexts.isEmpty {
                Button("Add Default GTD Contexts") {
                    self.addDefaultContexts()
                }.foregroundColor(.accentColor)
                    .accessibility(identifier: "addDefaultContexts")
            }
        }
    }

    /// Deletes the contexts at the specified index locations in `contexts`.
    func delete(at offsets: IndexSet) {
        for index in offsets {
            let context = contexts[index]
            context.delete()
        }
        DataManager.shared.saveAndSync()
    }

    /// Adds the contexts from "Getting Things Done"
    func addDefaultContexts() {
        for name in ["Calls", "At Computer", "Errands", "At Office", "At Home", "Anywhere", "Agendas", "Read/Review"] {
            let context = ContextMO(context: managedObjectContext)
            context.name = name
        }
        DataManager.shared.saveAndSync()
    }
}
Run Code Online (Sandbox Code Playgroud)

防止变更/冲突

这应该通过您的数据模型来完成。要使用 WWDC2019 中的示例,假设您正在编写一个博客应用程序,并且您有一个“posts”实体:

Post
----
content: String
Run Code Online (Sandbox Code Playgroud)

如果用户同时在两台设备上修改“内容”,一台设备会覆盖另一台设备。

相反,让内容成为“贡献”:

Content
-------
post: Post
contribution: String
Run Code Online (Sandbox Code Playgroud)

然后,您的应用程序将读取这些贡献并使用适合您的应用程序的策略合并它们。最简单/最懒惰的方法是使用 modifiedAt 日期并选择最后一个。

对于我上面提到的应用程序,我选择了几个策略:

  • 对于简单的字段,我只是将它们包含在实体中。最后一位作家获胜。
  • 对于笔记(即大字符串 - 大量数据丢失),我创建了一个关系(每个项目有多个笔记),并允许用户向一个项目添加多个笔记(自动为用户添加时间戳)。这既解决了数据模型问题,又为用户添加了类似 Jira 注释的功能。现在,用户可以编辑现有笔记,在这种情况下,最后一个写入更改的设备“获胜”。

显示“首次运行”(例如入职)屏幕

我将为此提供几种方法:

  • 在 UserDefaults 中存储首次运行标志。如果标志不存在,请显示您的首次运行屏幕。这种方法使您的首次运行成为每个设备的事情。也给用户一个“跳过”按钮。(来自Detect first launch of iOS app 的示例代码)

      let launchedBefore = UserDefaults.standard.bool(forKey: "launchedBefore")
      if launchedBefore  {
          print("Not first launch.")
      } else {
          print("First launch, setting UserDefault.")
          UserDefaults.standard.set(true, forKey: "launchedBefore")
      }
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果用户以前使用过您的应用程序,则在表中设置一个 FetchRequestController 肯定会有数据。如果您的提取结果为空,则显示您的首次运行屏幕,如果您的 FetchRequestController 触发并有数据,则将其删除。

我推荐 UserDefaults 方法。这更容易,如果用户刚刚在设备上安装了你的应用程序,这是一个很好的提醒,如果他们几个月前安装了你的应用程序,玩了一会儿,忘记了,买了一部新手机,在上面安装了你的应用程序(或发现它是自动安装的),然后运行它。

杂项

为了完整起见,我将添加 iOS 14 和 macOS 11 向 NSPersistentCloudKitContainer 添加一些通知/发布者,让您的应用程序在同步事件发生时得到通知。尽管您可以(并且可能应该)使用它们来检测同步错误,但使用它们来检测“同步已完成”时要小心。

这是使用新通知的示例类。

import SwiftUI

/// The user's list of contexts, plus an add button
struct NextActionsLists: View {

    /// The Core Data enviroment in which we should perform operations
    @Environment(\.managedObjectContext) var managedObjectContext

    /// The available list of GTD contexts to which an action can be assigned, sorted alphabetically
    @FetchRequest(sortDescriptors: [
        NSSortDescriptor(key: "name", ascending: true)]) var contexts: FetchedResults<ContextMO>

    var body: some View {
        Group {
            // User-created lists
            ForEach(contexts) { context in
                NavigationLink(
                    destination: ContextListActionListView(context: context),
                    label: { ContextListCellView(context: context) }
                ).isDetailLink(false)
                    .accessibility(identifier: "\(context.name)") // So we can find it without the count
            }
            .onDelete(perform: delete)

            ContextAddButtonView(displayComplicationWarning: contexts.count > 8)

            if contexts.isEmpty {
                Button("Add Default GTD Contexts") {
                    self.addDefaultContexts()
                }.foregroundColor(.accentColor)
                    .accessibility(identifier: "addDefaultContexts")
            }
        }
    }

    /// Deletes the contexts at the specified index locations in `contexts`.
    func delete(at offsets: IndexSet) {
        for index in offsets {
            let context = contexts[index]
            context.delete()
        }
        DataManager.shared.saveAndSync()
    }

    /// Adds the contexts from "Getting Things Done"
    func addDefaultContexts() {
        for name in ["Calls", "At Computer", "Errands", "At Office", "At Home", "Anywhere", "Agendas", "Read/Review"] {
            let context = ContextMO(context: managedObjectContext)
            context.name = name
        }
        DataManager.shared.saveAndSync()
    }
}
Run Code Online (Sandbox Code Playgroud)

  • “在分布式系统中,您无法真正知道“同步是否已完成”,这可能是真的,但 NSPersistentCloudKitContainer 确实知道是否存在“待处理请求”,因此最好将其呈现给用户图标什么的。 (3认同)
  • @francisfeng - 要确定 iCloud 是否确实同步了数据,除了检查 NSPersistentCloudKitContainer 的通知之外,还需要检查网络状态和 iCloud 帐户状态。例如(或者对于已经为您执行此检查的库;-)),请参阅 https://github.com/ggruen/CloudKitSyncMonitor/blob/main/Sources/CloudKitSyncMonitor/SyncMonitor.swift 中的“syncStateSummary”属性,它检查网络可用性(使用 NWPathMonitor)和 iCloud 帐户可用性(使用 CKContainer.default().accountStatus)。 (2认同)