skip to Main Content

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


  1. 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 pass nil 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 new CKFetchRecordZoneChangesOperation 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 the fetchAllChanges set to its default of true. 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 to true. 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.

    Login or Signup to reply.
  2. Assuming you have implemented also the callbacks of CKFetchRecordZoneChangesOperation you must save the token received by the callbacks permanently for example in UserDefaults.

    A smart way to do that is a computed property

    var lastChangeToken: CKServerChangeToken? {
        get {
            guard let tokenData = UserDefaults.standard.data(forKey: Key.zoneChangeToken) else { return nil }
            return try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: tokenData)
        }
        set {
            if let token = newValue {
                let tokenData = try! NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
                UserDefaults.standard.set(tokenData, forKey: Key.zoneChangeToken)
            } else {
                UserDefaults.standard.removeObject(forKey: Key.zoneChangeToken)
            }
        }
    }
    

    The struct Key is for constants, you can add more keys like the private subscription ID etc.

    struct Key {
        let zoneChangeToken = "zoneChangeToken"
    }
    

    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. In init initialize the privateDatabase and also the zone properties.

    This is the async/await version of fetchLatestChanges, it returns the new records and also the deleted record IDs

    /// Using the last known change token, retrieve changes on the zone since the last time we pulled from iCloud.
    func fetchLatestChanges() 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]()
        
        while awaitingChanges {
            /// Fetch changeset for the last known change token.
            let allChanges = try await privateDatabase.recordZoneChanges(inZoneWith: zone, since: lastChangeToken)
            
            /// Convert changes to `CKRecord` objects and deleted IDs.
            let changes = allChanges.modificationResultsByID.compactMapValues { try? $0.get().record }
            changes.forEach { _, record in
                changedRecords.append(record)
            }
            
            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)
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search