How to Fetch Records from CloudKit Not in My Local Cache - cloudkit

I have an app that uses CloudKit for sync and I maintain a local cache of records. I have run into a sync scenario that I can't figure out.
I'm using the Public database and when my app is opened, I want to be able to go get all the updated records that my app missed while it was closed, or on a device where the app was just installed.
I can get the updated records by creating a NSPredicate to compare the modificationDate like this:
let predicate = NSPredicate(format: "modificationDate > %#", syncTimestamp as CVarArg)
let query = CKQuery(recordType: recordType, predicate: predicate)
But the part I can't figure out is how to get only the records that have been added to, or removed from, the CloudKit server.
Does anyone know how to do this? I know Apple provides a way for this in the Private database but I'm using the Public one here.
The only thing I can think of so far is to query all the reocrds of a certain recordType, collect all their recordNames and compare to my local cache. But it'd be nice to have smarter way than just pull large amounts of data and comparing huge arrays of recordNames.

CKQuerySubscription(recordType: myRecordType, predicate: predicate, options: [.firesOnRecordCreation, .firesOnRecordDeletion]) works perfectly on public DB.
Here's a code snippet (saving subscription is done with RxCloudKit, but this is beyond the point) -
let predicate = NSPredicate(format: "TRUEPREDICATE")
// the options are different from what you need, just showcasing the possibilities:
let subscription = CKQuerySubscription(recordType: recordTypeTest, predicate: predicate, options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion, .firesOnce])
let info = CKNotificationInfo()
info.alertLocalizationKey = "NEW_PARTY_ALERT_KEY"
info.soundName = "NewAlert.aiff"
info.shouldBadge = true
subscription.notificationInfo = info
self.publicDB.rx.save(subscription: subscription).subscribe { event in
switch event {
case .success(let subscription):
print("subscription: ", subscription)
case .error(let error):
print("Error: ", error)
default:
break
}
}.disposed(by: self.disposeBag)

Related

CloudKit: Query returns partial results, no errors

I have some kind of CloudKit indexing issue. When I save records to iCloud using CKModifyRecordsOperation, modifyRecordsCompletionBlock returns no errors. When I query those records using NSPredicate(value: true) or Dashboard, most of the time it misses one or two records.
So say I upload 5 records (no errors), wait some time (~15 secs) to make sure that indexes are updated, and then query them (through dashboard or app's CKQueryOperation). Most of the time it will show 4 records out 5. Again, no errors. Records are in privateDB in customZone.
Here is what's strange: I'm always able to get records that query didn't return by manually typing recordNames in Dashboard (development) under 'Fetch' menu. So it stores them, just doesn't query. When I delete indexes in a dashboard and reassign them, dashboard query will start to return all the results (with previously omitted records too), but after a few more uploads, some will start to be missing from query again.
Here is my CKModifyRecordsOperation:
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: [])
operation.modifyRecordsCompletionBlock =
{ [weak self] savedRecords, deletedRecordIDs, error in
guard error == nil else { // no errors here ... }
...
//for each item uploaded to iCloud, mark as synced
if let savedRecords = savedRecords{ // all attempted to save records are here
PendingCloudOperations.shared.markAsUploaded(
savedRecords.map{ $0.recordID.recordName })
}
completion(...)
}
operation.savePolicy = .changedKeys // tried .allKeys too
operation.qualityOfService = .userInitiated
self.privateDB.add(operation)
I experimented with record fields (originally date, asset, and reference) trying to see if any of the fields make a problem. But even if I remove all field's (creating a record with no extra fields, just system meta), problem persists. I didn't include CKQueryOperation code, because Dashboard acts same way as the app.
Any ideas?
EDIT:
Here are bare-bones of my fetching function:
var receipts:[FS_Receipt] = []
let query = CKQuery(recordType: myRecordType, predicate: NSPredicate(value: true))
let operation = CKQueryOperation(query: query)
//completion block
operation.queryCompletionBlock = { [weak self] cursor, error in
guard error == nil else {
// doesn't have any errors here
}
completion(...)
}
operation.recordFetchedBlock = { record in
// doesn't return all records here most of the time.
}
operation.qualityOfService = .userInitiated // without this, 'no internet' will NOT return error
operation.resultsLimit = 5000
operation.zoneID = customZoneID
self.privateDB.add(operation)
}

Fetching a one to many relationship using Core Data

iam a swift beginner, i wanna code a little app for me and i have a problem with it. My app has three entities (Templates, Records and Positions), here you can see:
Datamodel
At one part of the app i can add new Records and for that Record i add some Positions (xpos and ypos).
At a part I have a tableview were i list my Records. Now i wanna click one Record in the table and i want to get all Position-Attributes linked with the particular selected Record.
With that code i can get all xpos-Positions but how can i get a single xpos? :)
guard let moc = self.managedContext else {
return
}
let fetchRequest: NSFetchRequest<Records> = Records.fetchRequest()
do {
let searchResults = try moc.fetch(fetchRequest)
let xpos = searchResults[0].positions?.mutableArrayValue(forKey: "xpos")
print(xpos)
} catch {
print("Error with request: \(error)")
}
You want to look into using NSPredicate. NSPredicate basically lets you define the conditions which the record needs to meet to be included in the results. Think of it as a filter if you will.
fetchRequest.predicate = NSPredicate(format: "uniqueID = %#", arugments:...)

CKSubscription returns error that query must have at least one firing mode when using FiresOnce

I have this code. Which works when creating a subscription with a firing mode of FiresOnRecordCreation... but getting error on FiresOnce.. the error is "Query Subscriptions must have at least one type of firing mode"
My goal is that I have a photo. When the first rating of the photo occurs i want to get a notification. I do not want to use FiresOnRecordCreation on the reference as for every rating there will be a notification which is too many. I just want the first notification for the first rating received.
let database = CKContainer.defaultContainer().publicCloudDatabase
let predicate = NSPredicate(format:"owningPhoto == %#", ref)
let subscription = CKSubscription(recordType: "PhotoRatings", predicate: predicate, options: .FiresOnce)
xcode crashes on the subscription but on with FiresOnce.
besides the .FireOnce you should also indicate if that's for creation, update or deletion. So the call should be something like:
let subscription = CKSubscription(recordType: "PhotoRatings", predicate: predicate, options: [.FiresOnce, .FiresOnRecordCreation, .FiresOnRecordUpdate, .FiresOnRecordDeletion])

swift request coredata and delete

i would like to request all my core data where the "mhd" field >= the date of today. this code works fine:
func DatenAbrufen() {
let calendar = NSCalendar.currentCalendar()
let AktuellesDatum = calendar.startOfDayForDate(NSDate())
let fetchRequest = NSFetchRequest(entityName: "LM_ITEMS")
fetchRequest.predicate = NSPredicate(format: "mhd >= %#", AktuellesDatum)
if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LM_ITEMS] {
lebensmittel = fetchResults
}
}
how can i delete in this query all other data, where "mhd" is not >= date of today?
If you need to support iOS 8.x, or OS X Yosemite, or older, you'll need to fetch each object and then delete it. Write another predicate that is the opposite of the one you have. Execute that fetch request. Then iterate over the found objects and send each one a deleteObject. Save your managed object context. This can take quite a while, if you're deleting lots of objects.
For iOS 9 and El Capitan, you can use NSBatchDeleteRequest, described in WWDC 2015 session 220. For large numbers of deleted objects, NSBatchDeleteRequest is much faster. You're not loading each object into memory just to delete it. You also bypass some validation rules, and must refresh your MOC for it to see changes–which might be undesired consequences, depending on your usage.

CloudKit - CKQueryOperation results different each time the same query is ran

This is the case - I'm using a simple UITableView that renders records from the CloudKit publicDB. When I run the app, the query operation returns for example returns 2 results (that's all it has currently).
My table view has a refresh control and when I pull to refresh I got zero results, if I continue to do reloads, eventually a result might come out but now always.
The same thing happens with more results as well, I used to have CKLocation type which I queried and the response was always different without any common sense
Some example code (the predicate in this case is TRUEPREDICATE - nothing fancy):
let sort = NSSortDescriptor(key: "creationDate", ascending: false)
let query = CKQuery(recordType: "Tests", predicate: DiscoveryMode.getPredicate())
query.sortDescriptors = [sort]
var operation = CKQueryOperation(query: query)
if lastCursor != nil {
operation = CKQueryOperation(cursor: lastCursor)
}
operation.resultsLimit = 15
operation.recordFetchedBlock = recordFetchBlock
operation.queryCompletionBlock = { [weak self] (cursor:CKQueryCursor!, error:NSError!) in
if cursor != nil {
self!.lastCursor = cursor
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
Misc.hideLoadingInView(view: self!.view)
self!.tableView.reloadData()
self!.refreshControl.endRefreshing()
if error != nil {
Misc.showErrorInView(view: self!.view, message: error.localizedDescription)
}
})
}
CloudKit.sharedInstance.publicDB.addOperation(operation)
All the recordFetchBlock does is to add objects to a mutable array that the table view uses as dataSource.
I'm new to CloudKit and I'm puzzled is this by design (not returning all the results but some random) or I'm doing something wrong?
I see that you are using a cursor. because of that the 2nd call will start at the point where the first call ended. You have a resultsLimit of 15. When using a cursor, you will only receive records the 2nd time you execute the query if there were more than 15 records. To test if this is the issue just comment out the line where you set the cursor: operation = CKQueryOperation(cursor: lastCursor)
I found the issue, I was trying to do (in the NSPredicate) a radius with a value I read somewhere is in kilometers. Therefore I was trying to query records within 500 meters instead of 500 kilometers and the GPX file I'm using in the simulator has multiple records with a larger distance. Since it simulates movement, that was the reason not to get consistent results.
Now, when I'm using a proper value for the radius all seems to be just fine!