Query with compare predicate in CloudKit - ios

I have some CKRecords in Cloudkit, the records has two property of int type, which may be called 'cost' and 'price', I want to query records whose 'cost' > 'price', but the application crashed when I write a query like this:
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"MyRecordType" predicate:
[NSPredicate predicateWithFormat:#"cost > price"]];
this is the crash info given by Xcode:
Terminating app due to uncaught exception 'CKException', reason: 'Invalid predicate, Invalid right expression, is not a function expression
Please Help, thanks in advance.

OK,
Thought some more about this.
You query all records, but select only two fields; the cost and the price.
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Whatever", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["cost","price"]
Then you do your query [which should be quicker], sort out the records you want and then go get them with publicDB.fetchRecordWithID.
Yes, I know more than one query, in fact it looks like a lot more, but wait I think there is an answers in WWDC 2015 Tips & Tricks in Cloudkit; Watch the session and you might find something in there too. Here the code to fetch a record, if you have the record ID.
publicDB.fetchRecordWithID(), completionHandler: {record, error in
if error != nil {
println("there was an error \(error)")
} else {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.plasticOne.text = (record.objectForKey("subCategory") as String)
}
}
})
Sorry I cannot give you more than that without writing the code myself, and if I do that than you end up using my answer, and I sure you're answer is going to be better :)

Related

Can't Query Single CloudKit Record by recordName UUID

I have a CloudKit app where records are stored locally in CoreData. When creating the CoreData record, a UUID is generated and populated to the system recordName field. The app creates and saves records in CoreData then uploads the records to CloudKit. This works fine.
However, I am now coding to modify a record and am not able to fetch a record by the recordName UUID. I have not been successful setting a predicate to search for only that record. I can retrieve all records with TRUEPREDICATE and I can also retrieve a single record from a field I created in the CloudKit record type. I created a field called myRecordName where I store the same UUID as the CloudKit recordName. A query using the myRecordName works fine.
I have shown three methods below for the predicates. If p1 and p2 are not both commented out the app crashes on running. I assume I am missing something really simple here. Any guidance would be appreciated. Xcode 10.2.1, iOS 12.2, testing on a real device.
If either p1 or p2 or both are not commented out, the console shows:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CKReference rangeOfString:]:
unrecognized selector sent to instance 0x280878520'
func queryDatabase() {
let recordZone = CKRecordZone(zoneName: "PrimaryZone")
let recName = "0E763775-5AD3-4A50-BFB8-AC310180E8A2"
let recID = CKRecord.ID(recordName: recName, zoneID: recordZone.zoneID)
let searchRecord = CKRecord(recordType: "Patient", recordID: recID)
let p1 = NSPredicate(format: "%K == %#", CKRecord.Reference(recordID: recID, action: CKRecord_Reference_Action.none))
let p2 = NSPredicate(format: "%K == %#", CKRecord.Reference(record: searchRecord, action: CKRecord_Reference_Action.none))
//let predicate = NSPredicate(format: "TRUEPREDICATE")
let p3 = NSPredicate(format: "myRecordName = %#", recName)
let query = CKQuery(recordType: "Patient", predicate: p3)
let sortDescriptor = NSSortDescriptor(key: "lastNameText", ascending: true)
query.sortDescriptors = [sortDescriptor]
privateDatabase.perform(query, inZoneWith: recordZone.zoneID) { (results, error) in
if error != nil {
print("error querying database\(String(describing: error?.localizedDescription))")
} else {
DVC.ckRecords = results!
//print(results!)
print(DVC.ckRecords.first!)
}//if error else
}//perform query block
}//queryDatabase
I've never used CloudKit, so maybe I'm wrong, but…
The two problem lines of code, p1= and p2=, attempt to create a NSPredicate using a format string. Each format string contains two var arg substitutions, aka placeholders: %K and %#. So for this to work, that format string argument must be followed by two more arguments: first a key path, which should be a string (%K), and second a object for its value (%#). For your purposes here, the key path is probably just an attribute name.
But you have only provided one more argument, a CKRecord.Reference object, which the system tries to parse for the first (attribute name, string) argument. The error you are getting is typical of what happens when the system tries to parse an object such as CKRecord.Reference when it was expecting a string. Swift may be quite type safe but old-fashioned NSPredicate var args functions are not :)
To fix this problem (and move on to the next one), you should provide the key path (argument name) argument, something like this:
let p1 = NSPredicate(format: "%K == %#", "recordID", CKRecord.Reference(recordID: recID, action: CKRecord_Reference_Action.none))
let p2 = NSPredicate(format: "%K == %#", "recordID", CKRecord.Reference(record: searchRecord, action: CKRecord_Reference_Action.none))
Since I don't understand exactly what you are doing, you may need to tweak those two lines a bit. But the point is that, given your format string, you need three arguments to NSPredicate(format:), and the middle one needs to be a string representing a key path or attribute name.

How to validate NSPredicate before calling fetch

I create a search NSPredicate using a custom format string. Sometimes the syntax of this string is wrong and when I execute a fetch using a NSFetchRequest with this predicate I get a NSInvalidArgumentException, for example when the key-path is incorrect. I would much rather prefer to validate this predicate (call some method that returns YES if format is ok), than have an exception thrown that crashes my app. What can I do?
To rephrase the inquiry into a simple question:
Can I validate the predicate without potentially crashing the app?
Here is example code in Swift:
let text = "price > 0.1" // ok, no syntax error
let text = "price > 'abc'" // error
let predicate = NSPredicate.init(format: text, argumentArray: [])
let request = NSFetchRequest<Fruit>.init(entityName: "Fruit")
request.predicate = predicate
request.fetchLimit = 1
let fruit = try myContext.fetch(request) as [Fruit] // exception thrown here that crashes app
// sometimes the exception can be caught with an obj-c try/catch
// sometimes it can't be caught, and causes program to terminate
// CAN WE HAVE A SOLUTION THAT NEVER CAUSES A CRASH ?
There's no built-in way to validate that a predicate format string is a valid predicate. With format strings, you really need to validate the predicates by (a) testing them during development and (b) verifying that substitution values in predicates are at least the right data type.
If that's a problem, you might find it better to build your predicates in other ways, instead of with format strings. For example, use NSComparisonPredicate and NSCompoundPredicate to build the predicates from instances of NSExpression. The code will be much more verbose, but it will be easier to ensure that your predicate is valid by checking each part of it as you construct it.
You can make use of evaluate(with:) to validate NSPredicate's format,
Returns a Boolean value indicating whether the specified object matches the conditions specified by the predicate.
Swift 4.2:
For example,
let dataArray = [
"Amazon",
"Flipkart",
"Snapdeal",
"eBay",
"Jabong",
"Myntra",
"Bestbuy",
"Alibaba",
"Shopclue"
]
let searchString = "buy"
let predicate = NSPredicate(format: "SELF contains %#", searchString)
let searchResult = dataArray.filter { predicate.evaluate(with: $0) }
print(searchDataSource)
if !searchResult.isEmpty {
//success
} else {
//evaluation failed
}
To avoid an exception, you may want to use try-catch block

CloudKit cursors returning more results than expected

I am trying to query all the records of a specified RecordType in my CloudKit database. Because I may have more than 100 records of that type, I am using CloudKit cursors, which return records in batches until there are no more records to return (at which point the cursor is nil).
I save the number of records created on each creationDate. I am getting much higher numbers for each date than there are records in the CloudKit dashboard for those dates. For example, on one day when I count 110 records by hand on the CloudKit dashboard, my code says there are 256. On another day where I count 2, my code says there are 7.
Issues I've considered
Time zones differences - unlikely because these high counts occur even on days where there are no records created for several days before and after.
Maybe I'm counting the same records multiple times - I printed out the list of all 472 records that CloudKit found across all the cursor queries. From some testing, it would appear that there are not any record ID duplicates in that list, and all of the records are of the proper type. (Which seems so weird that I wonder if I need to look at that list more.)
Maybe there's something I'm misunderstanding with my use of cursors. Hence, this StackOverflow post.
Here's my code for how I'm working with cursors. If anyone sees anything off, please let me know!
// This is the main code that kicks off the cursor handling
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: recordType, predicate: predicate)
let sort = NSSortDescriptor(key: "creationDate", ascending: true) // so the 0th result is the earliest
query.sortDescriptors = [sort]
let operation1 = CKQueryOperation(query: query)
operation1.resultsLimit = 5
operation1.queryCompletionBlock = { (cursor, error) in
if error != nil {
self.recordTypeErrorHandling(error: error as! CKError, uid: uid, appID: appID, recordType: recordType)
}
else {
self.queryRecordsWithCursor(cursor: cursor, isFirstCheck: true, uid: uid, appID: appID, recordType: recordType)
}
CKContainer.default().publicCloudDatabase.add(operation1)
// Related functions
// adapted from: https://gist.github.com/evermeer/5df7ad1f8db529893f40
func queryRecordsWithCursor(cursor: CKQueryCursor?, isFirstCheck: Bool, uid: String, appID: String, recordType: String) {
guard let theCursor = cursor else { return }
let operation = CKQueryOperation(cursor: theCursor)
// happens each time a record is received
operation.recordFetchedBlock = { [recordType] record in
if self.recordTypeToRecordListDict[recordType] == nil {
self.recordTypeToRecordListDict[recordType] = [record]
}
else {
self.recordTypeToRecordListDict[recordType]?.append(record)
}
}
// happens when all records are done for that cursor
operation.queryCompletionBlock = { [recordType] cursor, error in
if error == nil {
if cursor == nil { // cursor is nil => we've gotten all records, so save them
self.saveRecordCounts(records: self.recordTypeToRecordListDict[recordType]!, uid: uid, appID: appID, recordType: recordType, isFirstCheck: isFirstCheck) // use isFirstCheck, not the value in the dictionary
}
else if self.recordTypeToRecordListDict[recordType] != nil {
self.queryRecordsWithCursor(cursor: cursor, isFirstCheck: isFirstCheck, uid: uid, appID: appID, recordType: recordType) // recursive call. if we've gotten here, there's definitely a non-nil cursor
}
}
else {
self.recordTypeErrorHandling(error: error as! CKError, uid: uid, appID: appID, recordType: recordType)
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
Please let me know if you need more of my code than this! Thank you!
P.S. For what it's worth, when I step through with the debugger, setting the results limit to 5 had no effect. Not sure if that's a related issue.
Figured it out. It was a combination of issues. One was that I didn't have a recordFetchedBlock for first set of records. Another issue was that I had a for-loop that needed a DispatchGroup to synchronize things.
Another important thing I learned - If you query CloudKit records in the dashboard, it's not necessarily the case that ALL of your records show up.

Powering Master-Detail with CloudKit

I have a UISearchResultsController that searches locally while typing, and remotely (CloudKit) when the search button is pressed. The number of results returned needs to be 10-30 (I'm currently testing my app with 25 and it's always enough)
The search results list is populated with RecordType1, while it's detail is populated by RecordType1 and RecordType2. My question is how to go about fetching the second reference type while minimizing my requests/sec. I was looking at Apple's CloudCaptions sample, and they solve the problem by fetching the second record type when each of RecordType1 is fetched. It seems that this needlessly creates fetch requests (1[RecordType1] + 25[RecordType2] = 26 requests). How can I reduce this? It seems like it should be possible in two requests (one for RecordType1 and then one to fetch all the RecordType2 associated with it).
UPDATE: RecordType2 has a back reference to RecordType1
Unless I am misunderstanding your problem, I think you can just execute a query on your CloudKit database:
let searchKey = ... // value for recordType1
let z = CKRecordZone.default()
let predicate = NSPredicate(format: "recordType1 == %#", searchKey)
let query = CKQuery(recordType: "recordType2", predicate: predicate)
db.perform(query, inZoneWith: z.zoneID) { (records, error) in
if error != nil {
// `records` contains recordType2
} else {
// check for errors
}
}
You can also search for a multiple keys using the IN comparison in the predicate:
let searchKeys = ... // [value1, value2, value3, etc.]
let predicate = NSPredicate(format: "recordType1 IN %#", searchKeys)
References
CloudKit CKQueryOperation (Apple)
CloudKit CKQuery (Apple)

CloudKit - NSPredicate for finding all records that contains multiple CKReferences in a reference list

I'm working on Joke app that uses CloudKit
Each joke has a reference list to some categories/tags.
I'm trying to query all jokes that has some specific tags. For instance I want to find all jokes that is in the categories of Animal and Doctor.
Right now I have tried with the following code
let tagRecords = tags.map { CKReference(record: $0.record, action: .None) }
let predicate = NSPredicate(format: "tags CONTAINS %#", tagRecords)
let query = CKQuery(recordType: "Jokes", predicate: predicate)
Basically what the above does is first of all creating an array of references and then make a predicate to find the tags the contains those references
Unfortunately this doesn't work
I get the following error
server message = "Internal server error""
So the question is: How do you find all records that contains all references in a reference list?
Assuming that tags is a relationship or an array of strings, you could try with the following predicate:
let predicate = NSPredicate(format: "ANY tags IN %#", tagRecords)
Hope this helps.

Resources