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

Core data NSPersistentCloudKitContainer:如何检查数据是否同步到CloudKit,core-data,cloudkit,ios13,nspersistentcloudkitcontainer,Core Data,Cloudkit,Ios13,Nspersistentcloudkitcontainer,我已经实现了NSPersistentCloudKitContainer以将我的数据同步到CloudKit,我想知道同步已经完成,没有其他待同步的更改 当我尝试重新安装应用程序时,我开始从CloudKit获取数据,它开始在控制台中打印某些日志。 从CloudKit获取所有数据大约需要30秒。一些日志提到了nscloudkitmrroringdelegate。看起来nscloudkitmrroringdelegate知道剩余的同步请求,但我找不到任何有关确保同步完成的信息 这里有几个日志显示NSCl

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

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

这里有几个日志显示NSCloudKitMirroringDelegate知道同步何时完成

CoreData:CloudKit:CoreData+CloudKit:-NSCloudKitMirroringDelegate checkAndExecuteNextRequest::检查挂起的请求。

CoreData:CloudKit:CoreData+CloudKit:-[NSCloudKitMirroringDelegate _排队请求:][u block_invoke(714):排队请求:A2BB21B3-BD1B-4500-865C-6C848D67081D

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

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


有没有办法知道数据已完全同步?我需要向用户展示特定的UI。

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

也就是说,这里有一些技术可以用来实现用例,这些用例往往会激发人们了解同步状态的欲望

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

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

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

以下是屏幕的快捷界面代码:

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()
    }
}
如果用户同时在两台设备上修改“内容”,其中一台将覆盖另一台

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

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

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

  • 对于简单字段,我只是将它们包含在实体中。最后一位作家获胜
  • 对于注释(即大字符串-大量数据将丢失),我创建了一个关系(每个项目有多个注释),并允许用户向一个项目添加多个注释(自动为用户添加时间戳)。这既解决了数据模型问题,又为用户添加了类似于Jira注释的特性。现在,用户可以编辑现有的便笺,在这种情况下,最后一个写更改的设备将“获胜”
显示“首次运行”(如上车)屏幕 我将给出几种方法:

  • 在UserDefaults中存储首次运行标志。如果没有该标志,则显示首次跑步屏幕。这种方法使您的第一次运行成为每设备的事情。也给用户一个“跳过”按钮。(示例代码来自)

  • 在一个表上设置FetchRequestController,如果用户以前使用过你的应用程序,该表中肯定会有数据。如果获取的结果为空,则显示第一次运行屏幕;如果FetchRequestController触发并有数据,则将其删除

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

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

下面是一个使用新通知的示例类

导入联合收割机
导入CoreData
@可用(iOS 14.0,*)
类同步监视器{
///我们为正在收听的发布者存储组合可取消内容,例如NSPersistentCloudKitContainer的通知。
fileprivate var disposables=Set()
init(){
NotificationCenter.default.publisher(用于:NSPersistentCloudKitContainer.eventChangedNotification)
.sink(receiveValue:{中的通知
如果让cloudEvent=notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey]
Post
----
content: String
Content
-------
post: Post
contribution: String
  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")
  }