0
votes

I'm creating an app that saves records to CloudKit and also stores the data locally in Core Data. I can indeed save records in both locations, but I need to be able to handle errors when a connection to iCloud is not available or there is a save error. I thought I could populate a variable within the CKModifyRecordsOperation completion block to differentiate, but that does not work - the return value from the CloudKit save is always false, even though the process succeeds. The idea is to save to CloudKit first, then retrieve the recordID and save that with the Core Data save.

Here's my code, I call doTheComboSave() from a barbutton. The console output is below the code. Any advice would be appreciated. Xcode 8.3.3, Swift 3, iOS 10.

func saveNewCloudKitRecord() -> Bool {

    privateDatabase = container().privateCloudDatabase
    recordZone = CKRecordZone(zoneName: "myPatientZone")

    var blockSavedToCloudKit = false

    let myRecord = CKRecord(recordType: "Patient", zoneID: (recordZone?.zoneID)!)
    myRecord.setObject(firstNameTextField.text as CKRecordValue?, forKey: "firstname")
    myRecord.setObject(lastNameTextField.text as CKRecordValue?, forKey: "lastname")
    let parentRefID = CKRecordID(recordName: "047EBE6C-AB1C-0183-8D80-33C0E4AE228B", zoneID: (recordZone?.zoneID)!)
            //
    //bunch more record fields
    //

    let modifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [myRecord], recordIDsToDelete: nil)
    modifyRecordsOperation.timeoutIntervalForRequest = 10
    modifyRecordsOperation.timeoutIntervalForResource = 10

    modifyRecordsOperation.modifyRecordsCompletionBlock = {

        records, recordIDs, error in

        if let err = error {
            blockSavedToCloudKit = false
            //create placeholder record name for later updating
        } else {
            blockSavedToCloudKit = true
            self.currentRecord = myRecord
            self.passedInCKRecord = myRecord
        }//if err

    }//modifyRecordsOperation

    privateDatabase?.add(modifyRecordsOperation)

    print("blockSavedToCloudKit is \(blockSavedToCloudKit)")
    return blockSavedToCloudKit

}//saveNewCloudKitRecord


typealias SavedCompletion = (_ success:Bool) -> Void

func saveTwoFiles(completionHandler : SavedCompletion) {

    let flag = saveNewCloudKitRecord()
    print("flag is \(flag)")

    completionHandler(flag)
    print("completionHandler(flag) is \(flag)")
}//makeTheComboSave

func doTheComboSave() {

    saveTwoFiles() { (success) -> Void in

        print("saveTwoFiles is \(success)")

        if success {
            //will pass the CKRecord so core data can store the recordID and recordName
            saveTheNewRecord()//this is the Core Data save

            DispatchQueue.main.async {
                self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
                print("Completion block has been run successfully.")
            }//Dispatch

        } else {
            //create placeholder recordName for later updating
            saveTheNewRecord()//this is the Core Data save
            DispatchQueue.main.async {
                self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
                print("Completion block has been run but the file save to CloudKit failed.")
            }//Dispatch


        }//if else
    }//block

}//doTheComboSave

Console Output:

blockSavedToCloudKit is false

flag is false

saveTwoFiles is false

completionHandler(flag) is false

Completion block has been run but the file save to CloudKit failed.

success in modifyRecordsOperation

currentRecordName is: B53DCFB8-0EFB-4E79-8762-FECCEFBA9BD8

1
I think you are not understanding the asynchronous nature of a CloudKit/network call. The false returned from saveNewCloudKitRecord is returned before the operation is finished. You would need a barrier or semaphore to make it return the way you expect in your example.agibson007

1 Answers

0
votes

See the comment from Ichydon above.

The saveNewCloudKitRecord() function returns before the completionHandler sets the boolean value I wanted to use. The whole idea was to create the CloudKit record first so that I could retrieve the recordName to store in Core Data in order to synchronize local and cloud records.

A better approach is to save to Core Data first but create the CKRecordID myself and feed that to the record when saving to CloudKit.

In the function to save the Core Data record:

let myRecordName = UUID().uuidString

Then return that string to use as a parameter for the CloudKit save.

let myRecordID : CKRecordID = CKRecordID(recordName: myRecordName, zoneID: (recordZone?.zoneID)!)

let myRecord = CKRecord(recordType: "Whatever", recordID : myRecordID)

Hope this helps someone else.