10
votes

I have managed to get CoreData with CloudKit working with the new NSPersistentCloudKitContainer on the iOS version of my app and got it to automatically sync while the app is running. However, when I went to set things up on the watchOS app, I noticed that a sync will only occur if I force close and reopen the watch app.

import Foundation
import CoreData

class DataManager : NSObject {
    static let shared = DataManager()

    #if os(watchOS)
    let transactionAuthorName = "watchOSApp"
    #else
    let transactionAuthorName = "iOSApp"
    #endif

    override private init() {
        super.init()
    }

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentCloudKitContainer(name: "Model")

        let cloudStoreUrl = applicationDocumentDirectory()!.appendingPathComponent("product.sqlite")

        let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreUrl)
        cloudStoreDescription.shouldInferMappingModelAutomatically = true
        cloudStoreDescription.shouldMigrateStoreAutomatically = true
        cloudStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier:"iCloud.com.company.product")

        container.persistentStoreDescriptions = [cloudStoreDescription]

        container.loadPersistentStores(completionHandler: { storeDescription, error in
            if let error = error as NSError? {
                print("Error loading store. \(error)")
            }
        })

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.automaticallyMergesChangesFromParent = true

        try? container.viewContext.setQueryGenerationFrom(.current)
        container.viewContext.transactionAuthor = transactionAuthorName

        return container
    }()
}

// MARK: - Core Data
extension DataManager {

    func applicationDocumentDirectory() -> URL? {
        return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:"group.shiningdevelopers.h2o")
    }

    func managedObjectContext() -> NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    func reset() {
        managedObjectContext().reset()
    }

    func saveContext () {
        let context = managedObjectContext()
        if context.hasChanges {
            do {
                try context.save()
            } catch let error as NSError {
                // Replace this implementation with code to handle the error appropriately.
                // Log this error for now to be able to glean more information
                print("Could not save. \(error), \(error.userInfo)")
            }
        }
    }
}

The following scenarios work - User has both the watchOS app and iOS app running - User makes a change to the data on the watch app - The change is reflected on the iOS app - User has only the iOS app running - User makes a change to the data on the iOS app - The user opens the watchOS app from terminated - The changes are reflected on the watchOS app

The following scenario does not work - User has the watchOS app running and the iOS app running - User makes a change to the data on the iOS app - No change ever makes it through to the watchOS app, even after waiting a long time - Only if I force close the app and restart does a sync occur

In the cases were a sync is successful I see the following logs correctly:

CoreData: debug: CoreData+CloudKit: 
-[PFCloudKitImporterZoneChangedWorkItem newMirroringResultByApplyingAccumulatedChanges:]_block_invoke_2(243): <PFCloudKitImporterZoneChangedWorkItem: 0x16530fb0> { ( "<CKRecordZoneID: 0x1656a920; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>" ) } - Importing updated records: ( "<CKRecord: 0x16526280; recordType=CD_LogEntry, values={\n "CD_day" = 15;\n "CD_entityName" = LogEntry;\n "CD_glassesGoal" = 8;\n "CD_glassesLogged" = 13;\n "CD_lastModified" = "2019-09-15 18:56:08 +0000";\n "CD_month" = 9;\n "CD_year" = 2019;\n}, recordChangeTag=7d, recordID=2180D6A3-ACFC-4421-8CAF-6EE288DAAC2E:(com.apple.coredata.cloudkit.zone:defaultOwner)>" ) Deleted RecordIDs: {
}

In the last scenario, however, I don't see any logs, it's completely quiet. I am sharing the same CoreData/CloudKit code in both the iOS and watchOS versions of the app. I am also using a NSFetchedResultsController to make sure my UI stays up to date and it seems to be working on the iOS app but not the watchOS app. Not sure if there was some step that I had missed when setting up the watch extension.

Has anyone gotten syncing to watchOS to work? Any help would be appreciated.

3
Hi! Can you share sample how to sync CoreData between iPhone and Apple Watch? I try to make it, but all time get auth error (wrong bandle ID) on Watch side, - Ku6ep

3 Answers

2
votes

Have you enabled context.automaticallyMergesChangesFromParent in your UI context?

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

context.automaticallyMergesChangesFromParent = true
2
votes

I had the same issue and I was able to solve it by configuring the default persistent store description instead of defining a new one:

lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentCloudKitContainer(name: "Model")
        // Change this variable instead of creating a new NSPersistentStoreDescription object
        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("No Descriptions found")
        }

        let cloudStoreUrl = applicationDocumentDirectory()!.appendingPathComponent("product.sqlite")

        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        description.url = cloudStoreUrl

        container.loadPersistentStores(completionHandler: { storeDescription, error in
            if let error = error as NSError? {
                print("Error loading store. \(error)")
            }
        })

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.automaticallyMergesChangesFromParent = true

        try? container.viewContext.setQueryGenerationFrom(.current)
        container.viewContext.transactionAuthor = transactionAuthorName

        return container
    }()
1
votes

Have you tried adding an observer like:

// Observe Core Data remote change notifications.
    NotificationCenter.default.addObserver(
        self, selector: #selector(type(of: self).storeRemoteChange(_:)),
        name: .NSPersistentStoreRemoteChange, object: container)

And then observing the observer for changes and doing UI updates at that time?