我有一个核心数据的备份/恢复功能,定义如下:
import CoreData
import FirebaseCrashlytics
import Foundation
extension NSPersistentCloudKitContainer {
enum CopyPersistentStoreErrors: Error {
case invalidDestination(String)
case destinationError(String)
case destinationNotRemoved(String)
case copyStoreError(String)
case invalidSource(String)
}
func restorePersistentStore(from backupURL: URL) throws {
guard backupURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL")
}
var isDirectory: ObjCBool = false
if FileManager.default.fileExists(atPath: backupURL.path, isDirectory: &isDirectory) {
if !isDirectory.boolValue {
throw CopyPersistentStoreErrors.invalidSource("Source URL must be a directory")
}
} else {
throw CopyPersistentStoreErrors.invalidSource("Source URL must exist")
}
for persistentStoreDescription in persistentStoreDescriptions {
guard let loadedStoreURL = persistentStoreDescription.url else {
continue
}
let backupStoreURL = backupURL.appendingPathComponent(loadedStoreURL.lastPathComponent)
guard FileManager.default.fileExists(atPath: backupStoreURL.path) else {
throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupStoreURL)")
}
do {
var storeOptions = persistentStoreDescription.options
storeOptions.updateValue(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let configurationName = persistentStoreDescription.configuration
let storeType = persistentStoreDescription.type
// Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack.
// When restoring, it's necessary to use the current persistent store coordinator.
try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupStoreURL, sourceOptions: storeOptions, ofType: storeType)
// Add the persistent store at the same location we've been using, because it was removed in the previous step.
try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions)
} catch {
Crashlytics.crashlytics().record(error: error)
throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)")
}
}
}
func copyPersistentStores(to destinationURL: URL, overwriting: Bool = false) throws {
guard destinationURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidDestination("Destination URL must be a file URL")
}
var isDirectory: ObjCBool = false
if !overwriting, FileManager.default.fileExists(atPath: destinationURL.path, isDirectory: &isDirectory) {
if !isDirectory.boolValue {
throw CopyPersistentStoreErrors.invalidDestination("Destination URL must be a directory")
}
}
if overwriting, FileManager.default.fileExists(atPath: destinationURL.path) {
do {
try FileManager.default.removeItem(at: destinationURL)
} catch {
Crashlytics.crashlytics().record(error: error)
throw CopyPersistentStoreErrors.destinationNotRemoved("Can't overwrite destination at \(destinationURL)")
}
}
// Create the destination directory
do {
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
} catch {
Crashlytics.crashlytics().record(error: error)
throw CopyPersistentStoreErrors.destinationError("Could not create destination directory at \(destinationURL)")
}
for persistentStoreDescription in persistentStoreDescriptions {
guard let storeURL = persistentStoreDescription.url else {
continue
}
guard persistentStoreDescription.type != NSInMemoryStoreType else {
continue
}
let destinationStoreURL = destinationURL.appendingPathComponent(storeURL.lastPathComponent)
if !overwriting, FileManager.default.fileExists(atPath: destinationStoreURL.path) {
// If the destination exists, the replacePersistentStore call will update it in place. That's fine unless we're not overwriting.
throw CopyPersistentStoreErrors.destinationError("Destination already exists at \(destinationStoreURL)")
}
do {
// Replace an existing backup, if any, with a new one with the same options and type. This doesn't affect the current Core Data stack.
// The function name says "replace", but it works if there's nothing at the destination yet. In that case it creates a new persistent store.
// Note that for backup, it doesn't matter if the persistent store coordinator is the one currently in use or a different one. It could be a class function, for this use.
try persistentStoreCoordinator.replacePersistentStore(at: destinationStoreURL, destinationOptions: persistentStoreDescription.options, withPersistentStoreFrom: storeURL, sourceOptions: persistentStoreDescription.options, type: .sqlite)
} catch {
Crashlytics.crashlytics().record(error: error)
throw CopyPersistentStoreErrors.copyStoreError("\(error.localizedDescription)")
}
}
}
}
备份和恢复功能在本地按预期工作。但是,当与 CloudKit 一起使用时,我在控制台中收到以下错误,并且所有数据在重新启动后都会丢失,并被 iCloud 中存储的数据覆盖:
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate tearDown:]_block_invoke(808): <NSCloudKitMirroringDelegate: 0x302f9c960>: Told to tear down with reason: Stores Changed
随后:
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:](2302): <NSCloudKitMirroringDelegate: 0x302f9c960> - Attempting recovery from error: Error Domain=NSCocoaErrorDomain Code=134404 "(null)" UserInfo={NSDetailedErrors=(
"Error Domain=NSCocoaErrorDomain Code=134407 \"Request 'ECA8B22F-4F97-4D7E-B6B9-D61CC5F2C4D9' was cancelled because the store was removed from the coordinator.\"
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate resetAfterError:andKeepContainer:](605): <NSCloudKitMirroringDelegate: 0x302f9c960> - resetting internal state after error: Error Domain=NSCocoaErrorDomain Code=134404 "(null)" UserInfo={NSDetailedErrors=(
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _scheduleAutomatedImportWithLabel:activity:voucher:completionHandler:]_block_invoke(3633): <NSCloudKitMirroringDelegate: 0x302f9c960> - Finished automatic import - SandboxImport - with result: <NSCloudKitMirroringResult: 0x301cb1650> storeIdentifier: 2A4E461C-6CBD-4EB1-8C3B-C8487E191315 success: 0 madeChanges: 1 error: Error Domain=NSCocoaErrorDomain Code=134410 "(null)" UserInfo={NSUnderlyingError=0x301eef420 {Error Domain=NSCocoaErrorDomain Code=134404 "(null)" UserInfo={NSDetailedErrors=(
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest](3535): <NSCloudKitMirroringDelegate: 0x302f9c960>: Checking for pending requests.
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate resetAfterError:andKeepContainer:](605): <NSCloudKitMirroringDelegate: 0x302f9c960> - resetting internal state after error: Error Domain=NSCocoaErrorDomain Code=134404 "(null)" UserInfo={NSDetailedErrors=(
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _scheduleAutomatedImportWithLabel:activity:voucher:completionHandler:]_block_invoke(3633): <NSCloudKitMirroringDelegate: 0x302f9c960> - Finished automatic import - SandboxImport - with result: <NSCloudKitMirroringResult: 0x301cb1650> storeIdentifier: 2A4E461C-6CBD-4EB1-8C3B-C8487E191315 success: 0 madeChanges: 1 error: Error Domain=NSCocoaErrorDomain Code=134410 "(null)" UserInfo={NSUnderlyingError=0x301eef420 {Error Domain=NSCocoaErrorDomain Code=134404 "(null)" UserInfo={NSDetailedErrors=(
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest](3535): <NSCloudKitMirroringDelegate: 0x302f9c960>: Checking for pending requests.
我想知道
restorePersistentStore
函数中缺少哪个步骤来告诉 iCloud 使用新恢复的持久存储数据?
我想分享Apple DTS的答案。
“当使用 Core Data + CloudKit 存储时,Core Data 维护 Core Data 存储和 CloudKit 数据库之间的同步状态。当您用新存储替换存储时,Core Data 会丢失状态,因此不知道如何将新存储与关联的 CloudKit 数据库同步。
要使用备份存储作为新的事实,您需要删除CloudKit中的所有数据,并重新创建整个数据集,以便将其同步到CloudKit。
或者,如果您能够设法了解当前存储和备份存储之间的更改,则可以将更改应用到当前存储。”
建议向 Core Data 团队提交反馈以考虑作为 API 请求。位于FB13816791下