My app uses NSPersistentCloudKitContainer to implement Coredata and iCloud synchronization data
Recently, I received feedback from online users about data synchronization errors, and the error code is CKErrorDomain 12 .
In the App, I use NSPersistentCloudKitContainer.eventChangedNotification to monitor the synchronization status, the following is the specific code
NotificationCenter.default.publisher(for: NSPersistentCloudKitContainer.eventChangedNotification)
.sink(receiveValue: { notification in
if let cloudEvent = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey]
as? NSPersistentCloudKitContainer.Event {
let event = SyncEvent(from: cloudEvent)
}
})
.store(in: &disposables)
When the user feedbacks that the data cannot be synchronized, it can be seen from the above code that an error occurred when CoreData + iCloud was exporting data.
At the same time, cloudKitEvent.error does not contain any error information, only CKErrorDomain 12 this information, I don’t know how to troubleshoot the error at all.
What's even more frightening is that when the CKErrorDomain 12 error occurs, the app's synchronization service will stop immediately. Trying to restart the app or restart the phone, and turn off iCloud synchronization in the system, will not make the synchronization service work again.
Only uninstalling and reinstalling can completely solve this problem, but users will also lose some data after uninstalling, because when the error occurs, any data generated by the user has not been successfully synchronized to iCloud, so these data will be lost after uninstalling.
The following is the code for CoreDataStack initialization
private func setupContainer(allowCloudKitSync: Bool) -> NSPersistentCloudKitContainer {
let container = NSPersistentCloudKitContainer(name: containerName)
let privateStoreURL = containerURL.appendingPathComponent(privateStoreName)
let privateDescription = NSPersistentStoreDescription(url: privateStoreURL)
let privateOpt = NSPersistentCloudKitContainerOptions(containerIdentifier: identifier)
privateDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
privateDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
"allowCloudKitSync = \(allowCloudKitSync)".debugLog("CoreDataStack")
if allowCloudKitSync {
privateDescription.cloudKitContainerOptions = privateOpt
} else {
privateDescription.cloudKitContainerOptions = nil
}
container.persistentStoreDescriptions = [privateDescription]
container.loadPersistentStores(completionHandler: { [weak self] (storeDescription, error) in
if let error = error as NSError? {
self?.dbStatePublish.send(.loadError)
#if DEBUG
"Unresolved error \(error), \(error.userInfo)".debugLog("CoreDataStack")
#endif
} else {
if storeDescription.cloudKitContainerOptions == nil {
self?.coreDataState = .local
} else {
self?.coreDataState = .cloud
}
"load coredata status = \(String(describing: self?.coreDataState))".debugLog("CoreDataStack")
}
})
let options = NSPersistentCloudKitContainerSchemaInitializationOptions()
try? container.initializeCloudKitSchema(options: options)
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
NotificationCenter.default.addObserver(self,
selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
return container
}
Post
Replies
Boosts
Views
Activity
In the past few weeks, about Coredata, I added the database backup function. After going online, I received feedback from users that the app opened with a white screen and could not be started normally.
By investigating the reason why the App cannot be started, it is that there is no callback after loadPersistentStores is executed.
This problem has never been reported by users before, until I added the code for data backup, so I suspect it has something to do with data backup.
Below is the code for data backup
func backupPersistentStore(atIndex index: Int, fileName: String) throws -> TemporaryFile {
precondition(persistentStores.indices.contains(index), "Index \(index) doesn't exist in persistentStores array")
let sourceStore = persistentStores[index]
let backupCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
let intermediateStoreOptions = (sourceStore.options ?? [:])
.merging([NSReadOnlyPersistentStoreOption: true],
uniquingKeysWith: { $1 })
let intermediateStore = try backupCoordinator.addPersistentStore(
ofType: sourceStore.type,
configurationName: sourceStore.configurationName,
at: sourceStore.url,
options: intermediateStoreOptions
)
let backupStoreOptions: [AnyHashable: Any] = [
NSReadOnlyPersistentStoreOption: true,
NSSQLitePragmasOption: ["journal_mode": "DELETE"],
// Minimize file size
NSSQLiteManualVacuumOption: true,
]
let backupFilename = fileName + ".sqlite"
let backupFile = try TemporaryFile(creatingTempDirectoryForFilename: backupFilename)
try backupCoordinator.migratePersistentStore(intermediateStore, to: backupFile.fileURL, options: backupStoreOptions, withType: NSSQLiteStoreType)
return backupFile
}
When to back up
sceneDidEnterBackground use Operation with UIApplication.shared.beginBackgroundTask
class BackUpOpeartion: Operation {
override func main() {
LocalBackupManager.shared.backupSync()
}
static func canHandler() -> Bool {
let backupStrategy = LocalUserDefaults.standard.backupUiModel
let lastBackUpDate = LocalUserDefaults.standard.lastBackUpDate
let now = Date()
let sixHour: TimeInterval
#if DEBUG
sixHour = TimeInterval(0.15 * 60 * 60)
#else
sixHour = TimeInterval(backupStrategy.backupFrequency) // 6h
#endif
let backupDate = lastBackUpDate + sixHour
// Clean the database at most once per week.
guard now > backupDate else {
"It's not time to backup".debugLog()
return false
}
return true
}
static func doWhenEnterBackground() {
if !canHandler() {
return
}
var bgtToken = UIBackgroundTaskIdentifier.invalid
bgtToken = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(bgtToken)
})
guard bgtToken != .invalid else{
return
}
DispatchQueue.global(qos: .utility).async {
LocalBackupManager.shared.backupSync { succeed in
DispatchQueue.main.async {
if succeed {
LocalUserDefaults.standard.lastBackUpDate = Date()
}
UIApplication.shared.endBackgroundTask(bgtToken)
}
}
}
}
}
BGProcessingTask in AppDelegate
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: dbBackupId, using: nil) { task in
self.backupDb(task: task as! BGProcessingTask)
}
}
func scheduleDbBackup() {
if !BackUpOpeartion.canHandler() {
return
}
let request = BGProcessingTaskRequest(identifier: dbBackupId)
request.requiresNetworkConnectivity = false
request.requiresExternalPower = false
do {
try BGTaskScheduler.shared.submit(request)
"submit DbBackup".debugLog()
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func backupDb(task: BGProcessingTask) {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
let operation = BackUpOpeartion()
task.expirationHandler = {
queue.cancelAllOperations()
}
operation.completionBlock = {
let success = !operation.isCancelled
if success {
LocalUserDefaults.standard.lastBackUpDate = Date()
}
task.setTaskCompleted(success: success)
}
}
Since before the online backup, there is no feedback about the white screen of the app failing to start, so I feel it is related to the backup
I don't know under what circumstances it would cause CoreDataStack loadPersistentStores no completionHandler callback
can someone help me? Thanks!
I use TabView PageTabViewStyle with SwiftUI to display a pageview, when I swipe this TabView I find child view will Recall onAppear method Many times, Can someone tell me why?
This is my code
import SwiftUI
struct Pageview: View {
		
		@StateObject var vm = PageViewModel()
		
		var body: some View {
				VStack {
						
						DragViewBar().padding(.top, 14)
						
						TabView(selection: $vm.selectTabIndex) {
								
								TextView(index: "0").tag(0)
								TextView(index: "1").tag(1)
								TextView(index: "2").tag(2)
								TextView(index: "3").tag(3)
								TextView(index: "4").tag(4)
								TextView(index: "5").tag(5)
								
						}
						.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
						
				}
		}
}
struct TextView: View {
		
		let index: String
		
		var body: some View {
				VStack {
						Text(index)
				}
				.onAppear { print(index) }
				
		}
}
struct DragViewBar: View {
		var body: some View {
				Rectangle()
						.frame(width:36.0,height:5.0).foregroundColor(Color.blue)
						.cornerRadius(100)
		}
}
class PageViewModel: ObservableObject {
		@Published var selectTabIndex = 0
}
When I swipe this Tabview, The result of the console printing
0
0
0
0
0
0
0
0
0
0
0
1
2
1
1
1
1
1
1
2
0
0
0
0
0
0
2
1
1
1
1
1
1
2
The correct case is to print only once per swipe
It just has a problem in ios14.2, 14.1 will be ok, you can load my code in Github: https://github.com/werbhelius/TabViewBug
Xcode version: 12.1 (12A7403)
Device: iPhone 6s iOS 14.2
I think you can reproduce this problem on any device in iOS 14.2
I look forward to your help to solve this problem. Thank you