Migrating Data to App Groups Disables Icloud Syncing

Migrating Data to App Groups Disables iCloud Syncing

If you are still facing the same problem, you should add the following line for the storeDescription

storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.yourapp.identifier")

Source: https://developer.apple.com/videos/play/wwdc2019/202/

Following is my CoreDataStack:

import CoreData

class CoreDataStack {
// MARK: - Core Data stack

static var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentCloudKitContainer(name: "data")

let storeURL = URL.storeURL(for: "group.com.myapp", databaseName: "data")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.myapp")
container.persistentStoreDescriptions = [storeDescription]

container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()


// MARK: - Core Data Saving support

static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}

return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}

App Groups and iCloud in iOS

If you're already using iCloud, you don't need to use app groups, because your extension can access the same iCloud container as your app does. As long as your entitlements and provisioning are configured correctly, you can literally just use the same Core Data stack setup in the extension as in the app.

Apple's Lister demo project does this, but there's nothing special about it. Just use iCloud as usual.

App groups are usually necessary to share data between apps and extensions. But a big exception is when the data is already stored external to the app-- as with iCloud.

Should I use 2 PersistentContainers to allow selective Syncing?

Manage Multiple Stores section of Setting Up Core Data with CloudKit explains how to synchronize only part of data.

WWDC 2019 session – Using Core Data With CloudKit

What are some reliable mechanism to prevent data duplication in CoreData CloudKit?

There is no unique constraint feature once we have integrated with CloudKit.

The workaround on this limitation is

Once duplication is detected after insertion by CloudKit, we will
perform duplicated data deletion.

The challenging part of this workaround is, how can we be notified when there is insertion performed by CloudKit?

Here's step-by-step on how to be notified when there is insertion performed by CloudKit.

  1. Turn on NSPersistentHistoryTrackingKey feature in CoreData.
  2. Turn on NSPersistentStoreRemoteChangeNotificationPostOptionKey feature in CoreData.
  3. Set viewContext.transactionAuthor = "app". This is an important step so that when we query on transaction history, we know which DB transaction is initiated by our app, and which DB transaction is initiated by CloudKit.
  4. Whenever we are notified automatically via NSPersistentStoreRemoteChangeNotificationPostOptionKey feature, we will start to query on transaction history. The query will filter based on transaction author and last query token. Please refer to the code example for more detailed.
  5. Once we have detected the transaction is insert, and it operates on our concerned entity, we will start to perform duplicated data deletion, based on concerned entity


Code example

import CoreData

class CoreDataStack: CoreDataStackable {
let appTransactionAuthorName = "app"

/**
The file URL for persisting the persistent history token.
*/
private lazy var tokenFile: URL = {
return UserDataDirectory.token.url.appendingPathComponent("token.data", isDirectory: false)
}()

/**
Track the last history token processed for a store, and write its value to file.

The historyQueue reads the token when executing operations, and updates it after processing is complete.
*/
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken,
let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }

if !UserDataDirectory.token.url.createCompleteDirectoryHierarchyIfDoesNotExist() {
return
}

do {
try data.write(to: tokenFile)
} catch {
error_log(error)
}
}
}

/**
An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
*/
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()

var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}

static let INSTANCE = CoreDataStack()

private init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
error_log(error)
}
}
}

deinit {
deinitStoreRemoteChangeNotification()
}

private(set) lazy var persistentContainer: NSPersistentContainer = {
precondition(Thread.isMainThread)

let container = NSPersistentCloudKitContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.xxx)

// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})

// Provide transaction author name, so that we can know whether this DB transaction is performed by our app
// locally, or performed by CloudKit during background sync.
container.viewContext.transactionAuthor = appTransactionAuthorName

// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true

// TODO: Not sure these are required...
//
//container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//container.viewContext.undoManager = nil
//container.viewContext.shouldDeleteInaccessibleFaults = true

// Observe Core Data remote change notifications.
initStoreRemoteChangeNotification(container)

return container
}()

private(set) lazy var backgroundContext: NSManagedObjectContext = {
precondition(Thread.isMainThread)

let backgroundContext = persistentContainer.newBackgroundContext()

// Provide transaction author name, so that we can know whether this DB transaction is performed by our app
// locally, or performed by CloudKit during background sync.
backgroundContext.transactionAuthor = appTransactionAuthorName

// Similar behavior as Android's Room OnConflictStrategy.REPLACE
// Old data will be overwritten by new data if index conflicts happen.
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

// TODO: Not sure these are required...
//backgroundContext.undoManager = nil

return backgroundContext
}()

private func initStoreRemoteChangeNotification(_ container: NSPersistentContainer) {
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self,
selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator
)
}

private func deinitStoreRemoteChangeNotification() {
NotificationCenter.default.removeObserver(self)
}

@objc func storeRemoteChange(_ notification: Notification) {
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}

/**
Process persistent history, posting any relevant transactions to the current view.
*/
private func processPersistentHistory() {
backgroundContext.performAndWait {

// Fetch history received from outside the app since the last token
let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
historyFetchRequest.predicate = NSPredicate(format: "author != %@", appTransactionAuthorName)
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
request.fetchRequest = historyFetchRequest

let result = (try? backgroundContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction] else { return }

if transactions.isEmpty {
return
}

for transaction in transactions {
if let changes = transaction.changes {
for change in changes {
let entity = change.changedObjectID.entity.name
let changeType = change.changeType
let objectID = change.changedObjectID

if entity == "NSTabInfo" && changeType == .insert {
deduplicateNSTabInfo(objectID)
}
}
}
}

// Update the history token using the last transaction.
lastHistoryToken = transactions.last!.token
}
}

private func deduplicateNSTabInfo(_ objectID: NSManagedObjectID) {
do {
guard let nsTabInfo = try backgroundContext.existingObject(with: objectID) as? NSTabInfo else { return }

let uuid = nsTabInfo.uuid

guard let nsTabInfos = NSTabInfoRepository.INSTANCE.getNSTabInfosInBackground(uuid) else { return }

if nsTabInfos.isEmpty {
return
}

var bestNSTabInfo: NSTabInfo? = nil

for nsTabInfo in nsTabInfos {
if let _bestNSTabInfo = bestNSTabInfo {
if nsTabInfo.syncedTimestamp > _bestNSTabInfo.syncedTimestamp {
bestNSTabInfo = nsTabInfo
}
} else {
bestNSTabInfo = nsTabInfo
}
}

for nsTabInfo in nsTabInfos {
if nsTabInfo === bestNSTabInfo {
continue
}

// Remove old duplicated data!
backgroundContext.delete(nsTabInfo)
}

RepositoryUtils.saveContextIfPossible(backgroundContext)
} catch {
error_log(error)
}
}
}


Reference

  1. https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud - In the sample code, the file CoreDataStack.swift illustrate a similar example, on how to remove duplicated data after cloud sync.
  2. https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes - Information on transaction histories.
  3. What's the best approach to prefill Core Data store when using NSPersistentCloudKitContainer? - A similar question


Related Topics



Leave a reply



Submit