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

时间:2019-12-02 12:24:34

标签: 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。

1 个答案:

答案 0 :(得分:1)

要引用Apple开发者论坛中类似问题的“框架工程师”:“这是谬论”。在分布式系统中,您无法真正知道“同步已完成”,因为此时可能处于联机或脱机状态的另一台设备可能具有未同步的更改。

也就是说,您可以使用一些技术来实现用例,这些用例往往会促使人们渴望了解同步状态。

添加默认/样本数据

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

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

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

Screenshot of Next Actions screen

enter image description here

这是屏幕的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()
    }
}

防止更改/冲突

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

Post
----
content: String

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

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

Content
-------
post: Post
contribution: String

您的应用程序将读取供稿并使用适合您应用程序的策略将其合并。最简单/最懒惰的方法是使用ModifyAt日期并选择最后一个。

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

  • 对于简单字段,我只是将它们包括在实体中。最后一位作家获胜。
  • 对于笔记(即大字符串-丢失大量数据),我创建了一个关系(每个项目有多个笔记),并允许用户向一个项目添加多个笔记(为用户自动添加时间戳)。这既解决了数据模型问题,又为用户添加了类似于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")
      }
    
  • 在表上设置FetchRequestController,如果用户以前使用过您的应用程序,则该表中肯定会有数据。如果获取的结果为空,则显示您的首次运行屏幕,如果FetchRequestController触发并包含数据,则将其删除。

我建议使用UserDefaults方法。这更容易,可以预期是用户只是将您的应用程序安装在设备上,并且如果他们几个月前安装了您的应用程序,玩了一段时间,忘记了,换了新手机,在其上安装了应用程序,也可以提醒您。找到它的自动安装),然后运行它。

其他

出于完整性考虑,我将添加iOS 14和macOS 11向NSPersistentCloudKitContainer添加一些通知/发布程序,以便在发生同步事件时通知您的应用程序。尽管您可以(可能应该)使用它们来检测同步错误,但是请谨慎使用它们来检测“同步已完成”。

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

import Combine
import CoreData

@available(iOS 14.0, *)
class SyncMonitor {
    /// Where we store Combine cancellables for publishers we're listening to, e.g. NSPersistentCloudKitContainer's notifications.
    fileprivate var disposables = Set<AnyCancellable>()

    init() {
        NotificationCenter.default.publisher(for: NSPersistentCloudKitContainer.eventChangedNotification)
            .sink(receiveValue: { notification in
                if let cloudEvent = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey]
                    as? NSPersistentCloudKitContainer.Event {
                    // NSPersistentCloudKitContainer sends a notification when an event starts, and another when it
                    // ends. If it has an endDate, it means the event finished.
                    if cloudEvent.endDate == nil {
                        print("Starting an event...") // You could check the type, but I'm trying to keep this brief.
                    } else {
                        switch cloudEvent.type {
                        case .setup:
                            print("Setup finished!")
                        case .import:
                            print("An import finished!")
                        case .export:
                            print("An export finished!")
                        @unknown default:
                            assertionFailure("NSPersistentCloudKitContainer added a new event type.")
                        }

                        if cloudEvent.succeeded {
                            print("And it succeeded!")
                        } else {
                            print("But it failed!")
                        }

                        if let error = cloudEvent.error {
                            print("Error: \(error.localizedDescription)")
                        }
                    }
                }
            })
            .store(in: &disposables)
    }
}