0

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

JohnSF
  • 3,736
  • 3
  • 36
  • 72
  • 1
    Look at the individual partial errors. – rmaddy Aug 21 '17 at 21:49
  • As rmaddy says, look at the individual errors; To do this, implement the [per record completion block](https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation/1447470-perrecordcompletionblock) Do you have a CKAsset in the "bunch more fields"? – Paulw11 Aug 21 '17 at 21:53
  • I do have a CKAsset, however during testing it is always the same image and it is relatively small (less than 200kB). I will implement the per record completion block to look at the errors - I had not seen that. Thanks. – JohnSF Aug 21 '17 at 22:04
  • You don't need the "per record" completion block. You should be getting an error in the final completion block that represents the error code `partialFailure`. That will contain a dictionary of errors keys on the record ids of the failed records. – rmaddy Aug 21 '17 at 22:21
  • Paulw11, Maddy - you nailed it. There is some issue with the CKAsset field. I did implement the per record completion block and ALL of the errors were errors finding the asset file. I must have some issue with deleting the file before the completion gets to it. I will figure that out and post. In the meantime I will edit my post with the error in case someone else has this issue. Just for grins, I commented out the line to upload the CKAsset and ran a test with 50 records - no errors. Thanks. – JohnSF Aug 21 '17 at 22:41
  • Make sure you are writing your asset file with the `atomicWrite` option - https://stackoverflow.com/questions/43357769/saving-an-asset-list-array-to-specific-ckrecord/43380968#43380968 – Paulw11 Aug 21 '17 at 22:50
  • Related to clean Temp directory... Better use NSCachesDirectory in FileManager. Device will delete files in that directory at some point of time. – Adolfo Aug 22 '17 at 12:36
  • @user2698617 did you ever figure out what was the issue with your `CKAsset`? I'm having the same issue...CloudKit 75% fails to upload my `CKRecord` unless `I commented out the line to upload the CKAsset`...Each `CKRecord` contains a `CKAsset` image that is 6-7MB. – Pangu Jun 04 '18 at 19:08
  • Pangu - see the answer that I just posted. I was able to make the saves reliable by putting the asset process inside a sync queue. Hope this helps. – JohnSF Jun 05 '18 at 03:24

1 Answers1

2

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
JohnSF
  • 3,736
  • 3
  • 36
  • 72