NSPersistentCloudKitContainer:如何检查数据是否同步到CloudKit [英] NSPersistentCloudKitContainer: How to check if data is synced to CloudKit

查看:74
本文介绍了NSPersistentCloudKitContainer:如何检查数据是否同步到CloudKit的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have implemented NSPersistentCloudKitContainer to get my data synced to CloudKit, I would like to know that the sync is finished and there is no other change pending to be synced.

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

When I tried reinstalling the app, I start getting my data back from CloudKit and it started printing certain logs in the console. It takes around 30 seconds to get all my data back from the CloudKit. Some of the logs mention about NSCloudKitMirroringDelegate. It looks like NSCloudKitMirroringDelegate knows about the remaining sync requests but I couldn't find any information about being sure that the sync is complete.

以下几条日志确实显示NSCloudKitMirroringDelegate知道同步何时完成.

here are few logs which does show that NSCloudKitMirroringDelegate knows when sync is finished.

CoreData:CloudKit:CoreData + CloudKit:-NSCloudKitMirroringDelegatecheckAndExecuteNextRequest :: 检查待处理的请求.

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

CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _enqueueRequest:]_block_invoke(714): : enqueuing request: A2BB21B3-BD1B-4500-865C-6C848D67081D

CoreData:CloudKit:CoreData + CloudKit:-[NSCloudKitMirroringDelegatecheckAndExecuteNextRequest] _block_invoke(2085)::推迟其他工作.仍然有一个活动请求:A3E1D4A4-2BDE-4E6A-8DB4-54C96BA0579E

CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2085): : Deferring additional work. There is still an active request: A3E1D4A4-2BDE-4E6A-8DB4-54C96BA0579E

CoreData:CloudKit:CoreData + CloudKit:-[NSCloudKitMirroringDelegatecheckAndExecuteNextRequest] _block_invoke(2092)::没有其他请求执行.

CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2092): : No more requests to execute.

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

Is there any way to know that the data is synced completely? It is required for me to show certain UI to the user.

推荐答案

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

To quote "Framework Engineer" from a similar question in the Apple Developer forums: "This is a fallacy". In a distributed system, you can't truly know if "sync is complete", as another device, which could be online or offline at the moment, could have unsynced changes.

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

That said, here are some techniques you can use to implement the use cases that tend to drive the desire to know the state of sync.

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

Give them a button to add specific default/sample data rather than automatically adding it to the app. This both works better in a distributed environment, and makes the distinction between your app's functionality and the sample data clearer.

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

For example, in one of my apps, the user can create a list of "contexts" (e.g. "Home", "Work") into which they can add actions to do. If the user is using the app for the first time, the list of "Contexts" would be empty. This is fine, as they could add contexts, but it would be nice to provide some defaults.

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

Rather than detect first launch and add default contexts, I added a button that appears only if there are no contexts in the database. That is, if the user navigates to the "Next Actions" screen, and there are no contexts (i.e. contexts.isEmpty), then the screen also contains a "Add Default GTD Contexts" button. The moment a context is added (either by the user or via sync), the button disappears.

这是屏幕上的SwiftUI代码:

Here's the SwiftUI code for the screen:

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中的示例,请说您正在编写博客应用程序,并且有一个"posts"(帖子)实体:

Preventing changes/conflicts

This should be done via your data model. To use the example from WWDC2019, say you're writing a blogging app, and you have a "posts" entity:

Post
----
content: String

如果用户修改了内容",则同时在两个设备上,一个将覆盖另一个.

If the user modifies "content" on two devices at the same time, one will overwrite the other.

相反,将内容作为贡献":

Instead, make content a "contribution":

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

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

Your app would then read the contributions and merge them using a strategy appropriate for your app. The easiest/laziest approach would be to use a modifiedAt date and choose the last one.

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

For the app I mentioned above, I chose a couple of strategies:

  • 对于简单字段,我只是将它们包括在实体中.最后一位作家获胜.
  • 对于注释(即大字符串-丢失大量数据),我创建了一个关系(每个项目有多个注释),并允许用户向项目添加多个注释(为用户自动添加时间戳).这既解决了数据模型问题,又为用户添加了类似于Jira注释的功能.现在,用户可以编辑现有笔记,在这种情况下,最后一个写入更改的设备获胜".
  • For simple fields, I just included them in the entity. Last writer wins.
  • For notes (i.e. big strings - lots of data to lose), I created a a relationship (multiple notes per item), and allowed the user to add multiple notes to an item (which are automatically timestamped for the user). This both solves the data model issue and adds a Jira-comment-like feature for the user. Now, the user could edit an existing note, in which case the last device to write a change "wins".

我将提供几种方法:

  • 在UserDefaults中存储首次运行标志.如果该标志不存在,请显示您的首次运行屏幕.这种方法使您的首次运行成为每个设备的事情.给用户一个跳过"的提示.按钮.(示例代码来自检测到iOS应用首次启动)

  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触发并包含数据,则将其删除.

  • Set up a FetchRequestController on a table that will definitely have data in it if the user's used your app before. Display your first-run screens if the results of your fetch are empty, and remove them if your FetchRequestController fires and has data.

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

    I recommend the UserDefaults approach. It's easier, it's expected if the user just installed your app on a device, and it's a nice reminder if they installed your app months ago, played with it for a bit, forgot, got a new phone, installed your app on it (or found it auto-installed), and ran it.

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

    For completeness, I'll add that iOS 14 and macOS 11 add some notifications/publishers to NSPersistentCloudKitContainer that let your app be notified when sync events happen. Although you can (and probably should) use these to detect sync errors, be careful about using them to detect "sync is complete".

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

    Here's an example class using the new notifications.

    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)
        }
    }
    

    这篇关于NSPersistentCloudKitContainer:如何检查数据是否同步到CloudKit的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆