20

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.

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.

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

CoreData: CloudKit: CoreData+CloudKit: -NSCloudKitMirroringDelegate checkAndExecuteNextRequest: : Checking for pending requests.

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

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: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2092): : No more requests to execute.

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

4

2 回答 2

24

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

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

添加默认/示例数据

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

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

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

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

在此处输入图像描述

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

然后,您的应用程序将读取贡献并使用适合您的应用程序的策略将它们合并。最简单/最懒惰的方法是使用 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")
      }
    
  • 如果用户以前使用过您的应用程序,则在肯定会有数据的表上设置 FetchRequestController。如果您的 fetch 结果为空,则显示您的首次运行屏幕,如果您的 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)
    }
}
于 2020-09-16T20:00:44.830 回答
1

如果“向用户显示某些 UI”的意思是呈现引导屏幕以帮助用户预填充数据存储,则可以使用一种方法NSUbiquitousKeyValueStore,可以简单地描述为“UserDefaults在 iCloud 上”。

与方法相比有两个优点UserDefaults

  • 数据存储在 iCloud 上同步,因此如果用户已经在另一台设备上启动了您的应用程序,它将始终如一地工作
  • 您可以强制同步 ( NSUbiquitousKeyValueStore.default.synchronize(), 以确保数据已更新。

您可以使用它来存储“关键信息”,以便您知道会发生什么,并选择 UI 以相应地呈现 UI。例如,在具有帐户的应用程序中,您可以将帐户标识符哈希存储为数组。如果商店返回一个空数组,则您请求提示创建一个新帐户。否则,您将显示一个占位符以指示数据正在同步。然后让 FetchRequestControllers 在后台工作。

于 2022-02-04T09:20:15.810 回答