This is how I define fetching changes:
func fetchAllChanges(isFetchedFirstTime: Bool) {
let zone = CKRecordZone(zoneName: "fieldservice")
let options = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
options.previousServerChangeToken = Token.privateZoneServerChangeToken //initially it is nil
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zone.zoneID], configurationsByRecordZoneID: [zone.zoneID: options])
operation.fetchAllChanges = isFetchedFirstTime
operation.database = CloudAssistant.shared.privateDatabase
// another stuff
}
When I fetch all of them first time, then fetchAllChanges
is false
. So I only get server change token and save it for another use. No changes for records is returned. And it is ok;)
The problem is when I try to fetch it SECOND TIME. Since then nothing changed, server change token is not nil now, but fetchAllChanges
is true
because I need all the changes since first fetch (last server change token). It should work like this in my opinion.
But the SECOND TIME I got ALL THE CHANGES from my cloudkit (a few thousands of records and alll the changes). Why? I thought I told cloudkit that I do not want it like this. What am I doing wrong?
I have implemented @vadian answer, but my allChanges
is always empty. Why?
func fetchPrivateLatestChanges(handler: ProgressHandler?) async throws -> ([CKRecord], [CKRecord.ID]) {
/// `recordZoneChanges` can return multiple consecutive changesets before completing, so
/// we use a loop to process multiple results if needed, indicated by the `moreComing` flag.
var awaitingChanges = true
var changedRecords = [CKRecord]()
var deletedRecordIDs = [CKRecord.ID]()
let zone = CKRecordZone(zoneName: "fieldservice")
while awaitingChanges {
/// Fetch changeset for the last known change token.
print("🏆TOKEN: - (lastChangeToken)")
let allChanges = try await privateDatabase.recordZoneChanges(inZoneWith: zone.zoneID, since: lastChangeToken)
/// Convert changes to `CKRecord` objects and deleted IDs.
let changes = allChanges.modificationResultsByID.compactMapValues { try? $0.get().record }
print(changes.count)
changes.forEach { _, record in
print(record.recordType)
changedRecords.append(record)
handler?("Fetching (changedRecords.count) private records.")
}
let deletetions = allChanges.deletions.map { $0.recordID }
deletedRecordIDs.append(contentsOf: deletetions)
/// Save our new change token representing this point in time.
lastChangeToken = allChanges.changeToken
/// If there are more changes coming, we need to repeat this process with the new token.
/// This is indicated by the returned changeset `moreComing` flag.
awaitingChanges = allChanges.moreComing
}
return (changedRecords, deletedRecordIDs)
}
And here is what is repeated on console:
🏆TOKEN: – nil
0
🏆TOKEN: – Optional(<CKServerChangeToken: 0x1752a630; data=AQAAAAAAAACXf/////////+L6xlFzHtNX6UXeP5kslOE>)
0
🏆TOKEN: – Optional(<CKServerChangeToken: 0x176432f0; data=AQAAAAAAAAEtf/////////+L6xlFzHtNX6UXeP5kslOE>)
0
🏆TOKEN: – Optional(<CKServerChangeToken: 0x176dccc0; data=AQAAAAAAAAHDf/////////+L6xlFzHtNX6UXeP5kslOE>)
0
… …
This is how I use it:
TabView {
//my tabs
}
.tabViewStyle(PageTabViewStyle())
.task {
await loadData()
}
private func loadData() async {
await fetchAllInitialDataIfNeeded { error in
print("FINITO>>🏆")
print(error)
}
}
private func fetchAllInitialDataIfNeeded(completion: @escaping ErrorHandler) async {
isLoading = true
do {
let sthToDo = try await assistant.fetchPrivateLatestChanges { info in
self.loadingText = info
}
print(sthToDo)
} catch let error as NSError {
print(error.localizedDescription)
}
2
Answers
I believe you misunderstand how this works. The whole point of passing a token to the
CKFetchRecordZoneChangesOperation
is so that you only get the changes that have occurred since that token was set. If you passnil
then you get changes starting from the beginning of the lifetime of the record zone.The
fetchAllChanges
property is very different from the token. This property specifies whether you need to keep calling a newCKFetchRecordZoneChangesOperation
to get all of the changes since the given token or whether the framework does it for you.On a fresh install of the app you would want to pass
nil
for the token. Leave thefetchAllChanges
set to its default oftrue
. When the operation runs you will get every change ever made to the record zone. Use the various completion blocks to handle those changes. In the end you will get an updated token that you need to save.The second time you run the operation you use the last token you obtained from the previous run of the operation. You still leave
fetchAllChanges
set totrue
. You will now get only the changes that may have occurred since the last time you ran the operation.The documentation for
CKFetchRecordZoneChangesOperation
shows example code covering all of this.Assuming you have implemented also the callbacks of
CKFetchRecordZoneChangesOperation
you must save the token received by the callbacks permanently for example inUserDefaults
.A smart way to do that is a computed property
The struct
Key
is for constants, you can add more keys like the private subscription ID etc.Secondly I highly recommend to use the async/await API to fetch the latest changes because it get’s rid of the complicated and tedious callbacks.
As you have a singleton
CloudAssistant
implement the method there and use a property constant for the zone. Ininit
initialize theprivateDatabase
and also thezone
properties.This is the
async/await
version offetchLatestChanges
, it returns the new records and also the deleted record IDs