Getting a list of deleted CloudKit records with an expired change token

Usually, when you call fetchRecordZoneChanges with the previous change token, you get a list of the record ID’s that have been deleted since your last fetch.

But if you get a changeTokenExpired error because it‘s been too long since you last fetched, you have to call fetch again without a token.

For my specific application, I still need to know, though, if any records have been deleted since my last sync. How can I get that information if I no longer have a valid change token?

Answered by Frameworks Engineer in 873119022

The deletion history is maintained, you will receive the deleted records via the recordWithIDWasDeletedBlock callback.

The deletion history is maintained, you will receive the deleted records via the recordWithIDWasDeletedBlock callback.

Thanks, that's great to know!

Just to understand it better: it will usually return all deleted records since the last change token, but if I pass nil, then which deleted records will I get:

  • all deleted records ever?
  • all deleted records since device last received data?
  • something else?

Just want to anticipate how many record ID's I should expect.

It seems that my colleague hasn't had time to follow up, and so I'd like to jump in to hopefully help.

When you fetch changes with previousServerChangeToken being nil, CloudKit returns deleted record IDs via recordWithIDWasDeletedBlock if:

  • The deleted records were created on the device that is doing the fetch.
  • The records was concurrently deleted during the fetching of changes.

This design choice is to make sure that apps can maintain the synchronization between their local cache, if any, and the server.

That being said, a device that is not the "creator" of any deleted records won't get any deleted record ID.

I hope this clarifies the behavior but please feel free to follow up if anything unclear.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Hi Ziqiao, thanks for the reply. Let me explain the situation I'm facing with my CloudKit-powered app.

  1. Device A and B are both fully synced with the cloud.
  2. On Device A, the user deletes a bunch of records and continues to use the app, letting the app sync in the background.
  3. The user waits a few weeks before opening the app on Device B. When they open the app, it attempts to sync. The change token is expired, so it retrieves all of the records on the server.
  4. My app is designed to treat this as if this is the user's first sync (a "unify" sync), so it compares all records on the server with all local records, then uploads any local records missing from the cloud and downloads any cloud records missing from the local database.
  5. At this point, all of the items deleted in step 2 have been re-downloaded onto Device B.

Currently, my app never checks deletedRecordIDs on a unify sync. An easy fix would be to start processing this array even on unify syncs. But from your reply, it sounds like this will be an empty array on Device B because the deletions happened on Device A.

Is that true? If so, how can I resolve this problem? Unfortunately, it's somewhat common for my users to have a second device that they use very infrequently, and they get annoyed when previously deleted records re-appear on their second device.

To make my question more concrete, here's some CloudKit code demonstrating the scenario:

// device A
let newRecord = CKRecord(recordType: "Entry",
                         recordID: CKRecord.ID(recordName: "test"))
newRecord["name"] = "Length"
try await self.database.save(newRecord)
 
// device B
let (_, _, savedChangeToken, _) =
  try await self.database.recordZoneChanges(inZoneWith: self.zoneID, since: nil)
// successfully receives the Entry record with id "test" and saves locally
// we also persist savedChangeToken somewhere on device B
 
// device A
try await self.database.deleteRecord(withID: CKRecord.ID(recordName: "test"))
 
// several weeks pass
 
// device B (uses savedChangeToken from above, which has now expired)
let (_, deletions, _, _) =
  try await self.database.recordZoneChanges(inZoneWith: self.zoneID, since: savedChangeToken)
 
// is the following true or false?
deletions.map(\.recordID).contains(CKRecord.ID(recordName: "test"))

If it's false, how do I resolve the problem described above (deleted records re-appearing on the rarely-used device)?

  1. At this point, all of the items deleted in step 2 have been re-downloaded onto Device B.

This doesn't sound right. I'd expect the following behavior:

a. If the deletion on device A (step 2) has already synchronized, the records would have been deleted from the server, and so would not be available for device B to download.

b. Otherwise, the records deleted on device A still exist on the server, and so would indeed be downloaded to device B, and yet, once the changes on device A is synchronized, device B should get the deleted record IDs when it fetches changes with a valid server change token (and hence remove records from the local cache). So the data will eventually synchronized.

c. If the changes on device A is synchronized concurrently while device B is fetching with nil token, device B should get the deleted record IDs as well (and hence remove the records), as mentioned in the second bullet in my previous post.

If you observe a different behavior, please share the details and I'd take a closer look from there.

But from your reply, it sounds like this will be an empty array on Device B because the deletions happened on Device A. Is that true?

No, that's not why I said. In your case, when you fetch changes with a nil server change token from device B, the result set should include the deleted record IDs for the records that were originally created on device B. It's about which device did the creation, not the deletion.

If it's false, how do I resolve the problem described above (deleted records re-appearing on the rarely-used device)?

As I described above, the deleted record should either not appear at all, or appear and then disappear after the changes on device A is synchronized. Again, I'll be super curious about what happens if you see otherwise.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Getting a list of deleted CloudKit records with an expired change token
 
 
Q