CloudKit fetch operation returns old data set

488 views Asked by At

I am trying to create a very simple sycn between CloudKit and my local data store. In my function, I fetch the current data and compare it to the local data. New offline records go into one array and offline deletions go into another array. These are saved to CloudKit using CKModifyRecordsOperation. In the completion handler, I then call a perform operation to fetch the updated data.

Unfortunately, I do not get the new records in this fetch. The deletions are not included but I simply do not receive the new records from CloudKit. Here are the bare bones of my functions.

static func sync(
        completion: @escaping (
            _ results: [Pet],
            _ error: NSError?) -> ())
    {

    var petsToDelete = [CKRecordID]()
    var petsToSave = [CKRecord]()

    //Empty any existing pet array
    pets = []

    //load local pets into an array
    if let localPets = loadPetsLocal()  {

    //Set up the array for CloudKit Pets
    fetchFromCloud(){ (results , error) -> () in
        cloudPets = results

        //Use records marked for deletion to mark the CKRecords for deletion and put them into an array for saving to CloudKit
        for localPet in localPets {
            //MARK: RECON DELETIONS (Offline deletions of CKRecords)
            //Check to see if the record is marked for deletion (marked while off the CloudKit) and put them in an array
            if localPet.petName.lowercased().range(of: "delete me") != nil{
                //If the record is marked for deletion, iterate through the pets array to find the related CKRecord
                for petToDelete in cloudPets{
                    if localPet.recordName == petToDelete.recordID.recordName{
                        //Put this record into the deletion array of CKRecordIDs
                        petsToDelete.append(petToDelete.recordID)
                    }
                }
            }

        //Put all new records (recordName = "Local") into an array for saving to CloudKit
            if localPet.recordName == "Local" {
                changedRecord = CKRecord(recordType: RemoteRecords.pet, zoneID: ArendK9DB.share.zoneID)
                changedRecord?[RemotePet.petName] = localPet.petName as NSString
                changedRecord?[RemotePet.dob] = localPet.dob as NSDate?
                changedRecord?[RemotePet.petSex] = localPet.petSex as NSString?
                //Some special handling to get the UIImage into a CKAsset
                if let photo = localPet.photo {
                    let imageData:Data = UIImageJPEGRepresentation(photo, 1.0)!
                    let path:String = Pet.documentsDirectoryPath.appendingPathComponent(Pet.tempImageName)
                    try? UIImageJPEGRepresentation(photo, 1.0)!.write(to: URL(fileURLWithPath: path), options: [.atomic])
                    Pet.imageURL = URL(fileURLWithPath: path)
                    try? imageData.write(to: Pet.imageURL, options: [.atomic])
                    let File:CKAsset?  = CKAsset(fileURL: URL(fileURLWithPath: path))
                    changedRecord?[RemotePet.photo] = File as! CKAsset
                }
                petsToSave.append(changedRecord!)
            }
        }
        //There is a lot more code here to check for records that have changed

        **strong text**//If nothing changed, just return the original array
        if petsToDelete == [] && petsToSave == [] {
            print("DEBUG I have nothing to save")
            if error != nil {
                print(error?.localizedDescription ?? "General Query Error: No Description")
            } else {
                /*guard let records = results else {
                 return
                 }*/
                for record in results {
                    if let pet = Pet(remoteRecord: record) {
                        self.pets.append(pet)
                    }
                }
                completion(pets, nil)
            }
        } else {
            //Save the new and deleted records to CloudKit
            saveUpdateCloud(petsToSave: petsToSave, recordIDsToDeleve: petsToDelete)
                { (results , error) -> () in
                    if error != nil {
                        print(error?.localizedDescription ?? "General Query Error: No Description")
                    } else {
                        print("DEBUG: I have returned to the Recon from the update and isFinished is \(results) and pets is \(pets)")
                        //Grab the new, updated list of CKRecords
                        fetchFromCloud(){ (r , e) -> () in
                            print("DEBUG Loading new CloudKit array at \(Date())")
                            if e != nil {
                                print(e?.localizedDescription ?? "General Query Error: No Description")
                            } else {
                                for record in r {
                                    if let pet = Pet(remoteRecord: record) {
                                        pets.append(pet)
                                    }
                                }
                                //Save this new array locally
                                print("DEGUG Saving records to local store")
                                pets[0].saveToLocal(petsToSave: self.pets)
                                print("DEBUG I have a new pet array from CloudKit \(pets)")
                                completion(pets, nil)
                            }
                        }

                    }

                }
        }
    }
    } else {
        fetchFromCloud(){ (r , e) -> () in
            print("DEBUG There is no local data so I am returning a new CloudKit array  \(r)")
            if e != nil {
                print(e?.localizedDescription ?? "General Query Error: No Description")
            } else {
                for record in r {
                    if let pet = Pet(remoteRecord: record) {
                        pets.append(pet)
                    }
                }
                //Save this new array locally
                print("DEGUG Saving records to local store")
                pets[0].saveToLocal(petsToSave: self.pets)
                print("DEBUG I have a new pet array from CloudKit \(pets)")
                completion(pets, nil)
            }
        }
    }
}


//MARK: Fetch current records from CloudKit
static func fetchFromCloud(
    completion: @escaping (
    _ results: [CKRecord],
    _ error: NSError?) -> ())
{

    print("Fetch from CloudKit... starting completion handler")

    //Retrieve CloudKit data into an arry for reference
    let predicate = NSPredicate(value: true)
    let query = CKQuery(recordType: RemoteRecords.pet, predicate: predicate)
    ArendK9DB.share.privateDB.perform(query, inZoneWith: ArendK9DB.share.zoneID, completionHandler: {(records: [CKRecord]?, e: Error?)  in
        if e != nil {
            print(e?.localizedDescription ?? "General Query Error: No Description")
        } else {
            guard let records = records else {
                return
            }
            print("DEBUG Fetching from CloudKit... ending completion handler with records = \(records)")
            completion(records, nil)
            }
        }
    )


}

//MARK: Save new records and delete records for multiple Pets
static func saveUpdateCloud(petsToSave: [CKRecord]?, recordIDsToDeleve: [CKRecordID]?,
    completion: @escaping (
    _ results: Bool,
    _ error: NSError?) -> ())
{
    //Execute the operation
    var savedRecordNames = [String]()
    let saveOperation = CKModifyRecordsOperation(recordsToSave: petsToSave, recordIDsToDelete: recordIDsToDeleve)
    saveOperation.perRecordCompletionBlock = {
        record, error in
        if error != nil {
            print(error!.localizedDescription)
        } else {
            print("Saving Record to Cloud: \(record)")
            savedRecordNames.append(record.recordID.recordName)
        }
    }

    saveOperation.completionBlock = {
        print("DEBUG In the completion block therefore isFinished is \(saveOperation.isFinished) at \(Date())")
        completion(saveOperation.isFinished, nil)

    }
    ArendK9DB.share.privateDB.add(saveOperation)
}
1

There are 1 answers

0
Clifton Labrum On

If you use a CKFetchRecordZoneChangesOperation, then there are completions blocks that come with that operation that fire for deleted records and changed records. Here's an example:

let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = previousChangeToken //<-- this is a local value you store to keep track of changes

let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [recordZoneID], optionsByRecordZoneID: [recordZoneID:options])

operation.recordChangedBlock = { record in
  //Changed records available in `record`
  //Use a function here and pass the changed record to your local storage
}

operation.recordWithIDWasDeletedBlock = { recordId, type in
  //Deleted recordIds show up here
  //Here is where you line up your recordId.recordName with the recordNames in your local cache
}

operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
  // Save new zone change token to disk
  previousChangeToken = token
}

operation.recordZoneFetchCompletionBlock = { (zoneId, token, _, _, error) in
  if let error = error {
    //Check error and try again
  }
  // Write this new zone change token to disk (again)
  previousChangeToken = token
}

operation.fetchRecordZoneChangesCompletionBlock = { (error) in
  if let error = error {
    //Check error and try again
  }
  //You can fire a callback here if we ever need to know when the sync is done
  print("All done fetching changed records!")
}

//This is whichever CloudKit database you are using
CKContainer(identifier: "...").privateCloudDatabase.add(operation)

All of this presupposes that you have a notification set up to get these changes as they happen (which is likely more efficient than fetching everything all at once).

I hope that helps. :)