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)
}
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: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. :)