Why are not all records being saved in a CKModifyRecordsOperation to save in CloudKit in iOS?

I have this code that saves records into CloudKit in iOS in an iPhone 8 Simulator, but not all records are there when I check the database after the save code succeeds in saving 688 records whether 'maxNumberOfRecordsToModify' equal 50 or 400. I am sure I did everything correctly.


    let privateDatabase = CKContainer.default().privateCloudDatabase
    let maxNumberOfRecordsToModify = 400


    func save(_ records: [CKRecord]) {
        
        if iCloudAvailable() {
            if records.count > maxNumberOfRecordsToModify {
                let sliceOfRecords = Array(records[0 ..< maxNumberOfRecordsToModify])
                let leftOverRecords = Array(records[maxNumberOfRecordsToModify ... records.count - 1])
                let operation = CKModifyRecordsOperation(recordsToSave: sliceOfRecords, recordIDsToDelete: nil)
                operation.savePolicy = CKModifyRecordsOperation.RecordSavePolicy.allKeys
                operation.qualityOfService = QualityOfService.userInitiated
                operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
                    if error == nil {
                        print("Batch saved records!")
                        save(leftOverRecords)
                    } else {
                        if let err = error as? CKError, let time = err.retryAfterSeconds {
                            print(err)
                            DispatchQueue.main.asyncAfter(deadline: .now() + time) {
                                save(sliceOfRecords)
                            }
                        } else {
                            print(error!)
                        }
                    }
                }
                privateDatabase.add(operation)
            } else {
                let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
                operation.savePolicy = CKModifyRecordsOperation.RecordSavePolicy.allKeys
                operation.qualityOfService = QualityOfService.userInitiated
                operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
                    if error == nil {
                        print("Batch saved records!")
                        printNumberOfRecords()
                    } else {
                        if let err = error as? CKError, let time = err.retryAfterSeconds {
                            print(err)
                            DispatchQueue.main.asyncAfter(deadline: .now() + time) {
                                save(records)
                            }
                        } else {
                            print(error!)
                        }
                    }
                }
                privateDatabase.add(operation)
            }
        }


    }


    func printNumberOfRecords() {
        
        let predicate = NSPredicate(value: true)
        let query = CKQuery(recordType: DatabaseNameStrings.recordTypeAffirmation, predicate: predicate)
        
        privateDatabase.perform(query, inZoneWith: nil) {
            
            (records: [CKRecord]?, error: Error?) in
            
            if error != nil {
                
                print(error as Any)
                
            } else {
                
                if let records = records {
                    
                    print("Number of records in CloudKit=", records.count)
                    
                }
                
            }
            
        }
        
    }


Here is the debug window output:



Batch saved records!

Batch saved records!

Number of records in CloudKit= 32

Answered by rnikander in 416371022

A couple reasons...


1. If you start an op like `printNumberOfRecords` from that `modifyRecordsCompletionBlock` closure, not all the records will be there yet. I don't know how long you have to wait though.


2. You're calling `perform`, the docs for `perform` say not to use it if you have a lot of records. For me it only returns 100. Instead use CKQueryOperation and the cursor. Like this:


let privateDatabase = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: DatabaseNameStrings.recordTypeAffirmation, predicate: predicate)
let op = CKQueryOperation(query: query)
var recordCount = 0
func getChunk(_ op: CKQueryOperation, _ chunkNum: Int) {
    op.recordFetchedBlock = { rec in
        recordCount += 1
    }
    op.queryCompletionBlock = { cursor, error in
        print("finished chunk \(chunkNum). Count so far: \(recordCount)")
        if let error = error {
            print(error)
        } else if let c = cursor {
            let op = CKQueryOperation(cursor: c)
            getChunk(op, chunkNum+1)
        } else {
            print("Done. Record count = \(recordCount)")
        }
    }
    privateDatabase.add(op)
}
getChunk(op, 1)


That returns all 688 records, *if* you wait long enough.

Accepted Answer

A couple reasons...


1. If you start an op like `printNumberOfRecords` from that `modifyRecordsCompletionBlock` closure, not all the records will be there yet. I don't know how long you have to wait though.


2. You're calling `perform`, the docs for `perform` say not to use it if you have a lot of records. For me it only returns 100. Instead use CKQueryOperation and the cursor. Like this:


let privateDatabase = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: DatabaseNameStrings.recordTypeAffirmation, predicate: predicate)
let op = CKQueryOperation(query: query)
var recordCount = 0
func getChunk(_ op: CKQueryOperation, _ chunkNum: Int) {
    op.recordFetchedBlock = { rec in
        recordCount += 1
    }
    op.queryCompletionBlock = { cursor, error in
        print("finished chunk \(chunkNum). Count so far: \(recordCount)")
        if let error = error {
            print(error)
        } else if let c = cursor {
            let op = CKQueryOperation(cursor: c)
            getChunk(op, chunkNum+1)
        } else {
            print("Done. Record count = \(recordCount)")
        }
    }
    privateDatabase.add(op)
}
getChunk(op, 1)


That returns all 688 records, *if* you wait long enough.

That still doesn't fix the problem that the records don't get saved. When I query the database later, there still is not 688 records saved like it appears to have saved when I perform the CKModifyRecordsOperation. I actually checkd in CloudKit Dashboard. There really are 32 records only.

I actually ran your code in an Xcode unit test, with the change I made to "printNumberOfRecords", and it printed all 688. So, I'm out of ideas on what the problem could be.


By the way, while this is not the cause of the problem, there is a logic error in the example code - in the top clause where the `records.count > maxNumberOfRecordsToModify`, when it has an error it calls `save` again with `sliceOfRecords` instead of all the `records`. According to your output, it did not take that path. And it wouldn't result in 32 saves anyway; it would cause it forget about `leftOverRecords`.

I think my error was in checking the records after the save. The records were there, I just didn't retrieve it all, so it looks like not all are there. When I checked in CloudKit Dashboard, there were more not loaded. I was only looking at the first load before I loaded more.


By the way, your print number of records code was short one. I wonder if you forgot to count the last run of the getChunk.

Your print number of records is working now. When I ran it that one time it counted 687 insteaed of 688.

Maybe when it counted short it ran before all the records were saved.

Yeah, I mentioned in my original response that there is a time delay. I don't know if there is a way to programmatically wait for that to be ready. When I did it, I simply waited about 1 second, and then ran the `printNumberOfRecords` code.

If you want people to help you, you probably should give them credit when they answer your question, instead of marking your own response an answer.

Yeah. That's a good idea. I didn't think about that.


Ok. I marked your first answer as correct, because that was where you pointed out the problem.

Thanks.

You're welcome! 🙂

+1

Why are not all records being saved in a CKModifyRecordsOperation to save in CloudKit in iOS?
 
 
Q