How to Update Data in Tableview Without the Delay Using Cloudkit When Creating New Records

How to update data in TableView without the delay using CloudKit when Creating new Records

There is no guarantee as to when the record would be available in a query but there is something you can do. You can stitch the new record back in. Because when you create and save a record you have the record id you can make a ckfetchrecordsoperation and pass the id from the new record and you are guaranteed to get it back immediately. The indexing sometimes can take a while and this is frustrating with CloudKit. So basically the best way to guarantee a speedy database is make a query and if the new record id is not in there make a fetch with the id and append it to your results. Hope this makes sense.

I had to do this before and since I have not been too keen on CK. Here is the link to the operation to stitch the record back in. https://developer.apple.com/reference/cloudkit/ckfetchrecordsoperation also if you are using images check out this library I made that allows you to exclude the image data keys and download and cache on demand that could speed up your queries. https://github.com/agibson73/AGCKImage

Edit after comment:

I think the part you are not getting is the record may or may not come down with the query in viewcontroller 1 because of the way the indexing works. You even mention in your question it fetches old data. This is due to the server indexing. The same would happen if you deleted a record. It could still show up for some time in the query. In that case you keep track of the recently deleted record ids and remove them after the query. Again this manually adding and removing I am talking about is the only way to guarantee what the users see and the results from the query stay in sync with what the user would expect.

Here is some code although completely untested that I hope will help you visualize what I am saying above.

   func loadRecordsFromiCloud() {

// Get a private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "MyRecords", predicate: predicate)

privateDatabase.perform(query, inZoneWith: nil) { (results, error) in
if ((error) != nil) {
// Error handling for failed fetch from public database
print("error loading : \(error)")

}
else {
//check for a newRecord ID that might be missing from viewcontroller 2 that was passed back
if self.passedBackNewRecordID != nil{
let newResults = results?.filter({$0.recordID == self.passedBackNewRecordID})
//only excute if there is a new record that is missing from the query
if newResults?.count == 0{
//houston there is a problem
let additionalOperation = CKFetchRecordsOperation(recordIDs: [self.passedBackNewRecordID!])
additionalOperation.fetchRecordsCompletionBlock = { recordsDict,fetchError in
if let newRecords = recordsDict?.values as? [CKRecord]{
//stitch the missing record back in
let final = newRecords.flatMap({$0}) + results!.flatMap({$0})
self.reloadWithResults(results: final)
self.passedBackNewRecordID = nil

}else{
self.reloadWithResults(results: results)
self.passedBackNewRecordID = nil
}

}
privateDatabase.add(additionalOperation)
}else{
//the new record is already in the query result
self.reloadWithResults(results: results)
self.passedBackNewRecordID = nil
}
}else{
//no new records missing to do additional check on
self.reloadWithResults(results: results)
}

}
}
}

func reloadWithResults(results:[CKRecord]?){
self.tableViewDataArray = results!
DispatchQueue.main.async {
print("DispatchQueue.main.sync")
self.tableView.reloadData()
}

}
}

It's a bit of a mess but you can see that I am stitching the missing recordID if not nil back into the query that you are doing because that query is not guaranteed in real time to give you your expected new records. In this case self.passedBackNewRecordID is set based on the new recordID from Viewcontroller 2. How you set this or track this variable is up to you but you probably need an entire queue system because what I am telling you applies for changes to the record as well as deletes. So in a production app I had to track the records that had changes, deletes and additions and get the fresh version of each of those so you can imagine the complexity of a list of objects. Since I stopped using CloudKit because the tombstoning or indexing takes too long to show changes in queries.

To test your saved code could look like this.

 CloudKitManager.sharedInstance.privateDatabase.save(myRecord) { (savedRecord, error) -> Void in

if error == nil {

print("successfully saved record code: \(savedRecord)")
//save temporarily to defaults
let recordID = "someID"
UserDefaults.standard.set(recordID, forKey: "recentlySaved")
UserDefaults.standard.synchronize()
//now we can dismiss

}
else {
// Insert error handling
print("error Saving Data to iCloud: \(error.debugDescription)")
}
}

And in the code where you call the query in view controller 1 possibly viewWillAppear you could call this

func startQuery(){
UserDefaults.standard.synchronize()
if let savedID = UserDefaults.standard.value(forKey: "recentlySaved") as? String{
passedBackNewRecordID = CKRecordID(recordName: savedID)
//now we can remove from Userdefualts
UserDefaults.standard.removeObject(forKey: "recentlySaved")
UserDefaults.standard.synchronize()
}

self.loadRecordsFromiCloud()
}

This should fit your example pretty closely and allow you to test what I am saying with only minor changes possibly.

How to update all client records from CloudKit?

CloudKit has the CKFetchRecordChangesOperation for this. You can request all changes within a zone since the previous update. You can then synchronize that data with the storage inside your app.

If you do use subscriptions, then if there are multiple notifications send in a short period, there is a big chancre that your app won't get all notifications. Apple will limit that. This is why after processing received subscription notifications you should also execute a CKFetchNotificationChangesOperation after you received a notification.

Modify data with CloudKit

You can fetch, modify, and save changes you make to individual records.

The code snippet below shows how to fetch an Artwork record, changes the date attribute value, and saves it to the database.

// Fetch the record from the database
CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:containerIdentifier] publicCloudDatabase];
CKRecordID *artworkRecordID = [[CKRecordID alloc] initWithRecordName:@"115"];
[publicDatabase fetchRecordWithID:artworkRecordID completionHandler:^(CKRecord *artworkRecord, NSError *error) {
if (error) {
// Error handling for failed fetch from public database
}
else {
// Modify the record and save it to the database
NSDate *date = artworkRecord[@"date"];
artworkRecord[@"date"]; = [date dateByAddingTimeInterval:30.0 * 60.0];
[publicDatabase saveRecord:artworkRecord completionHandler:^(CKRecord *savedRecord, NSError *saveError) {
// Error handling for failed save to public database
}];
}
}];

Consider to read this article for more detailed information.

Blindly append value to CloudKit record

If you need to be able to append records without first getting the existing record, you might consider using a different record type and a CKReference.

For example, let's pretend your current record type is Company and it has a field of employees that is of type String (List). You want to add more employees to that list without first knowing who else is in the list.

You could make an Employees record type with a name field of type String and a company field of type CKReference. Then you can just create employees all you want and set their company reference and they'll be associated with their Company.

You don't have to know anything else about who else is employed by the company, but you can still query CloudKit for all the employees associated with a particular company.

All that said, it should be pretty easy to first get the record you are modifying, save the list of values to an array, append your new value, and save the array back to the CKRecord. :) Good luck!

How to detect when all CloudKit Updates happened?

You find your answer in the documentation here.

https://developer.apple.com/library/content/documentation/DataManagement/Conceptual/CloudKitQuickStart/SubscribingtoRecordChanges/SubscribingtoRecordChanges.html

In short you setup a subscription to cloudKit code and then register thru the appDelegate for remote notifications. And than finally setup the code to handle them when you get them.



Related Topics



Leave a reply



Submit