我已实现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。
答案 0 :(得分:1)
要引用Apple开发者论坛中类似问题的“框架工程师”:“这是谬论”。在分布式系统中,您无法真正知道“同步已完成”,因为此时可能处于联机或脱机状态的另一台设备可能具有未同步的更改。
也就是说,您可以使用一些技术来实现用例,这些用例往往会促使人们渴望了解同步状态。
给他们一个按钮以添加特定的默认/样本数据,而不是将其自动添加到应用程序中。这两种方法在分布式环境中均能更好地发挥作用,并使您的应用程序功能与示例数据之间的区别更加清晰。
例如,在我的一个应用程序中,用户可以创建“上下文”列表(例如“首页”,“工作”),并在其中添加要执行的操作。如果用户是第一次使用该应用程序,则“上下文”列表将为空。很好,因为他们可以添加上下文,但是最好提供一些默认值。
我没有检测首次启动并添加默认上下文,而是添加了一个按钮,该按钮仅在数据库中没有上下文时才会显示。也就是说,如果用户导航到“下一步操作”屏幕,并且没有上下文(即contexts.isEmpty
),则该屏幕还将包含“添加默认GTD上下文”按钮。添加上下文(由用户或通过同步)后,该按钮就会消失。
这是屏幕的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日期并选择最后一个。
对于我上面提到的应用,我选择了两种策略:
我将给出几种方法:
在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)
}
}