Search code examples
iosswiftcloudkit

CloudKit modifyRecordsCompletionBlock Fails Randomly


This app uses CloudKit for record storage and Core Data on the local device. Records created locally are saved to Core Data. The following method is called to send records that are created locally to CloudKit. I call this method when the app starts and when the app returns from the background. I also have programmed a button for the testing phase which calls this method.

The behavior seems to be completely random. For example if I create 10 records, some number of them are correctly uploaded to CloudKit and some number fail. If I rerun the method manually, eventually they all upload to CloudKit. I have been unable to determine ANY pattern for success or failure. At this point I seed test records from a method that makes the records all identical except for recordName(UUID) and a suffix for the text fields.

With limited testing if I create just one record it has always worked.

If I create just two records, an occasional failure.

If I create five records, there is almost always a failure.

Inside the modifyRecordsCompletionBlock, the updateSavedToCloudKitSuccess() and cleanUpTmpFile() always complete correctly regardless of whether the upload is successful.

I also tried increasing the timeouts and that makes absolutely no difference.

Any guidance would be appreciated. Xcode 8.3.3 iOS10 Swift 3

func saveNewCloudKitRecordFromCoreData( myRecordName : String ) {

    //get the Core Data record
    let patient = findPatientRecord(myRecordName: myRecordName)

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

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

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

    myRecord.setObject(myRecordName as CKRecordValue?, forKey: "myRecordName")
    myRecord.setObject(patient.firstName as CKRecordValue?, forKey: "firstname")
    //bunch more fields...

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

    modifyRecordsOperation.modifyRecordsCompletionBlock = {

        records, recordIDs, error in

        if let err = error {
            print("error in modifyRecordsOperation \(err.localizedDescription)")
            self.updateSavedToCloudKitSuccess(recordName: myRecordName, success: false)
            self.cleanUpTmpFile()
        } else {
            print("success in modifyRecordsOperation")
            self.currentRecord = myRecord
            self.updateSavedToCloudKitSuccess(recordName: myRecordName, success: true)
            self.cleanUpTmpFile()
        }//if err
    }//modifyRecordsOperation

    privateDatabase?.add(modifyRecordsOperation)

}//saveNewCloudKitRecordFromCoreData

Console output example:

NOresults.count is: 7

success in modifyRecordsOperation

savedToCloudKit = true

error in modifyRecordsOperation Failed to modify some records

savedToCloudKit = false

error in modifyRecordsOperation Failed to modify some records

savedToCloudKit = false

error in modifyRecordsOperation Failed to modify some records

savedToCloudKit = false

error in modifyRecordsOperation Failed to modify some records

savedToCloudKit = false

success in modifyRecordsOperation

savedToCloudKit = true

success in modifyRecordsOperation

savedToCloudKit = true

Based on the comments from rmaddy and paulw11 I implemented the perRecordCompletionBlock and found that the errors always are related to the CKAsset file. I will look to see if I cleanup that temporary file before a completion block has executed.

perRecordCompletionBlock: "; dateCreated = "2017-08-21 22:20:25 +0000"; dateOfBirth = "2017-04-23 22:20:25 +0000"; firstname = FirstName12; lastname = ZLastName12; myRecordName = "7621A7BD-7D32-4984-8117-2189D6F40D5F"; notes = "This is note number12"; patientlistparent = ""; primaryPhysician = "Dr. Who12"; ssan = 123456789; }, recordType=Patient>. Error: Optional() error in modifyRecordsOperation Failed to modify some records savedToCloudKit = false


Solution

  • I solved this issue by putting the asset save operation inside a sync global queue.

        DispatchQueue.global(qos: .userInitiated).sync {
    
            let tmpDir = NSTemporaryDirectory()
            let tmpFile = tmpDir.appending("test.png")
            let tmpFileURL = URL(fileURLWithPath: tmpFile)
    
            var asset : CKAsset?
    
            if patient.conditionImage != nil {
                let data = NSData(data: patient.conditionImage as! Data) as NSData
                do {
                    try data.write(to: tmpFileURL, options: .atomic)
                } catch {
                    print(error)
                }//do catch
    
                asset = CKAsset(fileURL: tmpFileURL)
            }//if there is a conditionImage
    
            myRecord.setObject(asset as CKRecordValue?, forKey: "conditionImage")
    
            DispatchQueue.main.async {
                //back on main
            }//
        }//DispatchQueue.global