I’ve setup the Cloudkit persistent container with private and shared database (see code below). I’ve enabled NSPersistentHistoryTrackingKey to true also for .shared database. I’ve noticed in the example from Apple that the History Tracking is only enabled in .private but not for .shared.
Questions:
For a CloudKit setup to sync
(a) between owners’ own devices (only private database), and
(b) between multiple iCloud Users through .private and .shared databases,
Do I need to enable history tracking for .shared database if I want to check the remote changes in the .shared database (or is the history tracking of the .private database of the owner also accessible in the .shared database)?
========================
let APP_BUNDLE_IDENTIFIER = Bundle.main.bundleIdentifier!
let APP_GROUP_IDENTIFIER = "group." + APP_BUNDLE_IDENTIFIER
private func setupPersistentContainer(_ container: NSPersistentCloudKitContainer? = nil, isStartup: Bool = true) -> NSPersistentCloudKitContainer {
let container = container ?? getCloudKitContainer(name: CORE_DATA_DATA_MODEL_NAME)
let defaultDirectoryURL: URL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_IDENTIFIER) ?? NSPersistentCloudKitContainer.defaultDirectoryURL()
let privateDataStoreURL = defaultDirectoryURL.appendingPathComponent("PrivateDataStore.store")
let sharedDataStoreURL = defaultDirectoryURL.appendingPathComponent("SharedDS.store")
// MARK: Private Store configuration
let privateDataStoreDescription = NSPersistentStoreDescription(url: privateDataStoreURL)
privateDataStoreDescription.configuration = "PrivateDataStore"
// Enable lightweight migration
privateDataStoreDescription.shouldInferMappingModelAutomatically = true
privateDataStoreDescription.shouldMigrateStoreAutomatically = true
// Turn History Tracking
privateDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let logOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: CLOUDKIT_LOG_CONTAINER_ID)
logOptions.databaseScope = .private
privateDataStoreDescription.cloudKitContainerOptions = logOptions
// turn on remote change notifications
privateDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [privateDataStoreDescription]
// MARK: Share Store configuration
let sharedDataStoreDescription = NSPersistentStoreDescription(url: sharedDataStoreURL)
sharedDataStoreDescription.configuration = "SharedDS"
// MARK: Enable lightweight migration
sharedDataStoreDescription.shouldInferMappingModelAutomatically = true
sharedDataStoreDescription.shouldMigrateStoreAutomatically = true
sharedDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let sharedOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: CLOUDKIT_LOG_CONTAINER_ID)
sharedOptions.databaseScope = .shared
sharedDataStoreDescription.cloudKitContainerOptions = sharedOptions
// turn on remote change notifications
sharedDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions.append(sharedDataStoreDescription)
self.stores = [StoreType : NSPersistentStore]()
container.loadPersistentStores(completionHandler: { [self] (storeDescription, error) in
if let error = error as NSError? {
print(error)
}
if let cloudKitContainerOptions = storeDescription.cloudKitContainerOptions {
if cloudKitContainerOptions.databaseScope == .private {
self.stores[.privateStore] = container.persistentStoreCoordinator.persistentStore(for: storeDescription.url ?? privateDataStoreURL)
} else if cloudKitContainerOptions.databaseScope == .shared {
self.stores[.sharedStore] = container.persistentStoreCoordinator.persistentStore(for: storeDescription.url ?? sharedDataStoreURL)
}
} else {
self.stores[.privateStore] = container.persistentStoreCoordinator.persistentStore(for: storeDescription.url ?? privateDataStoreURL)
}
})
/// Automatically merge changes in background context into View Context
/// Since we always use background context to save and viewContext to read only. The store values should always trump
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// Create separate context for read and write
container.viewContext.name = VIEW_CONTEXT_NAME
container.viewContext.transactionAuthor = self.contextAuthor
self.setQueryGeneration(context: container.viewContext, from: .current)
return container
}
Post
Replies
Boosts
Views
Activity
I encountered issues with some branch objects being assigned to multiple zones error (on iOS 17.5.1 and above). I get the errors when calling persistentContainer.shareshare(:to:completion:) and persistentContainer.persistUpdatedShare(:in:completion:).
"The operation couldn't be completed. Request '89D3F62D-548D-4816-9F1B-594390BD8F70' was aborted because the mirroring delegate never successfully initialized due to error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Object graph corruption detected. Objects related to 'Oxa2255fdc1fa980c5 x-coredata://CB800FA2-6054-4D91-8EBC-E9E31890344F/CDChildObject/p588' are assigned to multiple zones: {l <CKRecordZonelD: 0x3026a1170; zoneName=com.apple.coredata.cloud-kit.share.5D30F204-5970-489F-
BC2E-F863F1808A93, ownerName=defaultOwner>, <CKRecordZonelD: 0x302687b40; zoneName=com.apple.coredata.cloud-kit.zone, ownerName=_defaultOwner>"
In my setup, I moved all my root objects into one custom zone (there is only one custom zone in my private database). In one of my root object, there are 6 'one-to-one' and 2 'one-to-many' relationships. The branch objects can contains other relationships.
Create root object flow:
func saveToPersistent(_ object: ViewModelObject) {
serialQueue.async {
let context = backgroundContext()
context.performAndWait {
// Create new baby with its one-to-one child objects.
let cdNewBaby = self.newCDBaby(object, context)
if let share = self.getShareZone(.privateStore).first {
self.moveToShareZone(pObjects, share: share, store: .privateStore)
}
CoreDataManager.single.saveContext(context)
self.updateZoneNSaveContext([cdNewBaby], context: context)
} // context.perform
} // serialQueue.async
}
func backgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.transactionAuthor = contextAuthor
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
func getShareZone(_ storeType: StoreType, zoneName: String? = nil) -> [CKShare] {
var shares: [CKShare] = []
do {
shares = try persistentContainer.fetchShares(in: stores[storeType])
} catch {
print(error)
return shares
}
if let zoneName = zoneName {
shares = shares({ $0.recordID.zoneID.zoneName == zoneName })
}
return shares
}
func moveToShareZone(_ sharedObjects: [NSManagedObject], share: CKShare, store: StoreType) {
self.persistentContainer.share(sharedObjects, to: share) { managedObjects, share, container, error in
if let error = error {
print(error)
} else if let share = share, let store = self.stores[store] {
self.persistentContainer.persistUpdatedShare(share, in: store) { (share, error) in
if let error = error {
print(error)
}
}
}
}
} // moveToShareZone
Create one-to-many relationship branch object flow:
serialQueue.async {
let context = self.backgroundContext()
context.performAndWait {
// MARK: Retrieve the Root record
let pObjects = CDRootRecord.fetchRecord(rootRecord.uuidString, store: store, zoneName: zoneName, context: context)
if let pRootRecord = pObjects.first {
self.newCDLogContent(pRootRecord.self, viewModelObject: viewModelObject, context: context)
// MARK: Save Log
CoreDataManager.single.saveContext(context)
}
} // context.performAndWait
} // serialQueue
Questions:
(1) Should I save a root object first before share to custom zone; or share to custom zone first before save? (I implemented save before share to zone in the past and found some issues on iOS16 where the object is not saved; and end of sharing object before save which works)
(2) As I understand, if a branch record is saved under a root record, it should automatically go into the root record. Or do I have to also share the branch record to the custom zone?
My App uses Core Data with Cloudkit when users purchased subscription (and uses only Core Data without subscription). My App should normally appear under 'Settings > [User Name] > iCloud > Apps using iCloud - Show All'; however, some users feedback that they my App cannot access iCloud (a message I displayed in my App when CKContainer.default().accountStatus is NOT available or FileManager.default.ubiquityIdentityToken == nil); and they do not see my App name under 'iCloud > Apps using iCloud - Show All'. From the screenshot that one user provided, only Apps from Apple are displayed under 'Apps using iCloud' and no third-party Apps are shown. Since the user can access to 'iCloud > Apple using iCloud', they have signed in to iCloud successfully. They have also tried to restart my App and restart the phone but the iCloud accessibility issue still remains.
How is it possible that iCloud is accessible only to Apps from Apple but not to third party Apps? What can developer do to resolve iCloud accessibility issue here?