CloudKit: CKFetchRecordChangesOperation, CKServerChangeToken and Delta Download
Asked Answered
C

2

6

My question is related to the "Delta Download" thing as it was named in WWDC 2014 Advanced CloudKit.

I'm trying to make syncronization for my Core Data app, which is iPhone only for now (think: there is only one device active). So, basically the app will store user records in the cloud from one same device, for the most cases for now.

I have trouble with understanding custom zone feature which is based on CKFetchRecordChangesOperation aka Delta Download.

As I got it right, we have CKServerChangeToken's to maintain sync operations (I mean download only those records which was added/modified/deleted by another device), as was presented on WWDC. But, what I can't understand is that we recieve that token only after CKFetchRecordChangesOperation, when we save records to the cloud we don't get new token.

And if we make fetch with the current available token (since it changes only after fetch), we recieve records that was saved from our previous save operation. Basicaly we get save recods that already have on our device. Why? I'm missing something here?

What if we seeding some data to the cloud (from device A), it is justified for situation when device B is fetching the zone records, but what if device A be? Download all the records again?

I found recordChangeTag in the CKRecord, is this a property I can use for resolving conflicts with local objects - fetched objects (same or different version), if so can somebody give me example of how I need to do this: save recordChangeTag to Core Data when save record to CloudKit for the first time or how?

The lack of documentation is such a headache.

Castellany answered 5/5, 2016 at 23:2 Comment(6)
I actually opened a support case with Apple about this problem. It makes no sense that a device is told about changes that were made from the device using CKFetchRecordChangesOperation. I was told flatly - that's how it is. So your code needs to deal with getting all of those redundant record changes.Urbanist
@Urbanist Oh, thanks for this info. If you write it as answer, I will accept it since there is no other answers and your is the closest one.Castellany
@Urbanist I read your discussion on the apple devforum Rick, and I want to ask you, did you try the workaround that PBK suggested? Link to discussion: forums.developer.apple.com/message/77233#77233Castellany
I'm having the same issue, any luck?Brocky
@Brocky solved it, will write an answer later today or tomorrow. Basically you need to make one step after save operation to update the token.Castellany
@Coder1224, sorry for a long reply, I wrote an answer if you still curious.Castellany
C
4

I found a time to write an answer for this question. I won't dig into implementation, but I will discuss the concept.

CloudKit provides a way to data synchronisation between your device and the CloudKit server. What I use to establish synchronisation process in my case between iPhone and server only (again, if you have iPhone + iPad app, the process require more steps.):

I have custom zone in the private cloud database. I use OperationQueue to establish different asynchronous processes which depend on each other. Some operations have own operation queues.

Steps:

1) Check if my custom zone is exist

1.1) If there is no custom zone

1.2) Create new custom zone. (Optional: add records)

1.3) Refresh zone change token

You can refresh zone change token by: performing CKFetchRecordChangesOperation, fetchRecordChangesCompletionBlock returns CKServerChangeToken save it to UserDefaults (for example) using NSKeyedArchiver). This operation's task is to refresh token and it's performed at the end synchronisation process.

2) If there is custom zone already

2.1) Get changes from zone using previously saved zone change token. (CKFetchRecordChangesOperation)

2.2) Update and delete local records.

2.3) Refresh zone change token.

2.4) Check for local changes (I'm using last cloud sync timestamp to check what records was modified after).

2.5) Upload records to cloud kit database

2.6) Refresh zone change token again.

I highly recommend Nick Harris article series: https://nickharris.wordpress.com/2016/02/09/cloudkit-core-data-nsoperations-introduction/

You'll find there implementation and design concepts. It worth reading. I hope somebody'll find all of this helpful.

Castellany answered 21/1, 2017 at 14:7 Comment(3)
Thank you for taking time to write this up! It has help me with the same issue. Apple's WWDC video and documentation does not clarify or warn of this issue. They sell CloudKit as a god send framework but there are so many dead ends and gotchas.Icecold
Another suggestion to solve this issue ,if CoreData is used as the local storage, is that you can do a fetch request with all the changed records sent for the CKFetchRecordChangesOperation. Then filter out the records based on comparing the recordChangeTag, so even if the same records are returning they will be filtered out and proceed to process the actual changes.Icecold
@Icecold you are right, CloudKit kit have good opportunities, but lacks of documentation on some topics, like Core Data synchronisation with iCloud was... (and it is, but now it doesn't matter). About the filtering, yes, you need some kind of validation of the records. But, as I can see, CKFetchRecordChangesOperation is deprecated already... developer.apple.com/reference/cloudkit/…Castellany
L
2

As of iOS 13 there is a super helpful method in Core Data called NSPersistentCloudKitContainer. This method will automatically take care of all local caching and syncing with iCloud on private databases. You can set it up by simply changing

static var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "ShoeTrack")
    container.loadPersistentStores(completionHandler: {
        (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

to

static var persistentContainer: NSPersistentCloudKitContainer = {

    let container = NSPersistentCloudKitContainer(name: "ShoeTrack")
    container.loadPersistentStores(completionHandler: {
        (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

You will have to modify the Core Data Model file in your project and check "Use with CloudKit on each configuration.

Lavoisier answered 24/4, 2020 at 16:55 Comment(1)
Thanks for the answer @Jake, I wrote an article about migration to a NSPersistentCloudKitContainer and issues that I faced. Will add it here just in case it'll help someone: medium.com/@dmitrydeplov/…Castellany

© 2022 - 2024 — McMap. All rights reserved.