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

查看:41
本文介绍了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 checkAndExecuteNextRequest: : Checking for pending requests.

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

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

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

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天全站免登陆