How to validate NSPredicate before calling fetch - ios

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

Related

What predicate do I need to provide to return a CNGroup containing a given string in its name?

I'm trying to filter a list of CNGroups with a given string, but I'm struggling to work out how to form the correct predicate.
I'm fairly new to working with CNContactStore and struggling to find documentation that makes sense to me. I'm ok with predicates for CoreData, but this appears to be different. I'm working on the assumption that this is even possible.
let searchText = "Work"
let predicate = NSPredicate(format: "name CONTAINS[c] %#", searchText)
do {
let groups = try ContactStore.default.groups(matching: predicate)
completion(.success(groups))
} catch {
completion(.failure(error))
}

Generics method with predicated core data search won’t return correct result

I’m trying to have a generic method that search entities for a chosen string inside an attribute to see how many such entities are there, which then will decide what number or lack of number to attach to the string as it was returned. I’ve shown it in the picture. The entity type, the search string, and the attribute are all determined by input parameters. The problem is I cannot seem to make this generic method work consistently. On some projects it works, on others it doesn’t and the predicated search either returns an empty string or incomplete result. Can anyone help me figure it out? I’ve been scratching my head since last month.
Code:
private func getDefaultNameFor<T>Centityt T, defaultString: String, attribute: String)—>String{
var count = 0
var defaultName = defaultString
let type = T.self
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: “\(type)”)
let pred = NSPredicate(format: “%# CONTAINS[cd] %#", attribute, defaultString)
fetchRequest.predicate = pred
var untitledEntities: [NSManagedObject] = []
do {
untitledEntities = try context.fetch(fetchRequest)
}catch{
print("error fetching existing default names: \(error)”
}
count = untitledEntities.count
print("pred rEturns: " untitledEntities.description)
if count !=0 {
defaultName = defaultString + String(count)
}
print("default name: " defaultName)
return defaultName
}
Keypaths are substituted with %K instead of %#. (format: “%K CONTAINS[cd] %#", attribute, defaultString) worked.
Nothing wrong with using the generic, although I think not using it might still be preferable. This was a very basic sort of error I hope I had caught earlier by reviewing the predicate formatting guide.
Here’s how I solved it:
1: as suggested, I changed it from generic to taking a simple string as entity name, but the search still returns empty. I checked again at the pred but I don’t think it was formatted wrong. Am I missing something?
2: okay so I just adjusted the pred to be a simple format: “name CONTAINS[cd] ‘untitled’” and it worked fine. So why is it not working in (format: “%# CONTAINS[cd] %#", attribute, defaultString) ? I even tried to add ‘’ around the second %# but result was still empty.
3: (format: “name CONTAINS[cd] %#", defaultString) is working fine. So I guess the problem is the first substitute not functioning as keypath in the predicate format. So how to make it function as so?
4: Solved it! Keypaths are substituted with %K instead of %#. (format: “%K CONTAINS[cd] %#", attribute, defaultString) worked. This is such a basic error. I cannot believe I’ve been bugged with this for a month almost.

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.

Unable to fetch from coreData using Predicate as String?

I am unable to fetch data using a string as a direct argument to NSPredicate. Is there any difference between these two,
//Working case
fetchRequest.predicate = NSPredicate(format: "role == %#","GK")
In the above case, I am able to fetch the data with the predicate.
//Not Working
predicateString = String(format:"role == %#","GK")
fetchRequest.predicate = NSPredicate(format: predicateString)
Here I am unable to fetch the data.
The only difference between the above two cases is that I'm using a String variable to define the predicate. What is wrong here?
When the %# is replaced with a string variable when instantiating a NSPredicate the string variable is automatically surrounded by quotes so in your example the predicate becomes role == 'GK'
If you want to use String(format:...) you need to add the quotes yourself but in my opinion it's better to use NSPredicate(format:...) directly instead to avoid issues like this.
Format of NSPredicate is as following, where it expects a format, and format contains equaliser.
NSPredicate(format: <String>, <args: CVarArg...>)
let predicate1 = NSPredicate(format: "role == %#", "GK")
Here, if you will check the predicate1.predicateFormat, then you will get:
"role == \"GK\"",
which equilise role with string "GK" and return result in array.
But for,
let predicateString = String(format:"role == %#","GK")
let predicate2 = NSPredicate(format: predicateString)
You replaced the equaliser with simple string, which fails to return anything. You can check with predicate1.predicateFormat which will return:
"role == GK"
Here you can use "role == 'GK'" if you want to use string.
Hope it will help.

Using "timeIntervalSinceNow" in a predicate in SWIFT

I am trying to query for records that are created in the last 1 hour using Cloudkit (in SWIFT)
I have tried:
let predicate = NSPredicate(predicateWithFormat : "timeIntervalSinceNow %# < %f", "creationTime", -3600)
without success. The error message refers to a parsing error.
Anyone has a clue what would be the right format?
n.b creationTime is the name of the field in the Table
Your predicate isn't valid. Take a look at Apple's guide for creating NSPredicates.
You should use a key path on the left hand side of the expression. I assume that you have an array of objects, and these objects have a property named creationTime. Then the predicate should look like this:
NSPredicate(format: "creationTime.timeIntervalSinceNow > %d", -3600)
Note that I have used "greater than" > operator instead of < in the predicate you posted. That's because a date from the past will return a negative value from timeIntervalSinceNow method, as documentation states.
EDIT:
Seems you can't use key paths in CloudKit predicates. But I think you can create a reference date (an hour ago) and use it for comparison predicate:
let now = NSDate()
if let anHourAgo = NSCalendar.currentCalendar().dateByAddingUnit(
.HourCalendarUnit,
value: -1,
toDate: now,
options: NSCalendarOptions(0)) {
let predicate = NSPredicate(format: "creationDate > %#", anHourAgo)
let filtered = records.filteredArrayUsingPredicate(predicate!)
}

Resources