NSPredicate Formatting Issue - ios

I'm creating an iOS app where I want the user to be able to search through a UITableView and display results based on a variety of different inputs. I'm using an NSPredicate with multiple conditions separated by "OR" to do this, but for some reason it's only searching with the first condition and none of the others.
Here's how my NSPredicate is formatted:
let searchPredicate = NSPredicate(format: "(SELF.firstName CONTAINS[c]
%#) OR (SELF.lastName CONTAINS[c] %#) OR (SELF.major CONTAINS[c] %#) OR
(SELF.year CONTAINS[c] %#) OR (SELF.gpa CONTAINS[c] %#)",
searchController.searchBar.text!)
My parameters are firstName, lastName, major, year, and gpa. When I type in anything besides the first name in the search bar, no results show up. But typing in a first name does indeed return matches. Why is this happening? Is my NSPredicate formatted incorrectly?

You have five instances of the %# format specifier in your predicate so you need to pass five values. You are only passing one.
You need to repeat searchController.searchBar.text! five times. BTW - assign that to a safely unwrapped variable and repeat that variable five times. It will be less typing.
if let srch = searchController.searchBar.text {
let searchPredicate = NSPredicate(format: "(SELF.firstName CONTAINS[c] %#) OR
(SELF.lastName CONTAINS[c] %#) OR
(SELF.major CONTAINS[c] %#) OR
(SELF.year CONTAINS[c] %#) OR
(SELF.gpa CONTAINS[c] %#)", srch, srch, srch, srch, srch)
...
}

if let text = searchController.searchBar.text, text.characters.count > 0{
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "self.firstName CONTAINS[c]",text))
predicates.append(NSPredicate(format: "self.lastName CONTAINS[c]",text))
predicates.append(NSPredicate(format: "self.major CONTAINS[c]",text))
predicates.append(NSPredicate(format: "self.year CONTAINS[c]",text))
predicates.append(NSPredicate(format: "self.gpa CONTAINS[c]",text))
let searchPredicate:NSPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}

Not tested but this should work and is probably the simplest syntax for Swift if all your subpredicates are the same:
if let text = searchController.searchBar.text {
let myCompondPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: ["firstName", "lastName", "major", "year", "gpa"].map {
NSPredicate(format: "%K CONTAINS[c] %#", $0, text)
})
// Do something with myCompondPredicate here..
}

For a simple, static, predicate like this, I would opt for a native Swift approach:
import Foundation
struct Student {
let firstName: String
let lastName: String
let major: String
let gpa: Double
}
extension Student {
func anyFields(contain searchString: String) -> Bool {
return firstName.contains(searchString)
|| lastName.contains(searchString)
|| major.contains(searchString)
|| String(gpa).contains(searchString)
}
// Alternate implementation:
/*
func anyFields(contain searchString: String) -> Bool {
return [firstName, lastName, major, String(gpa)].contains(where: { field in
field.contains(searchString)
})
} */
}
let matchingStudents = students.filter(Student.anyFields(contain:))

Related

Swift - Search large array based on multiple conditions

I have an array of 4K+ items, using UISearchBar and UISearchController to sort the list for matches. I would like to get sorted array by few criteria, but most importantly by the order user types first
Search controller has 3 scope buttons for
category
subCategory
allCat
The items in the search array are using struct class to access the conditions:
struct Item {
var title: String
var category: String
var subCategory: String
var allCat: String
}
The standard method of filtering would be using something like this:
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
self.filteredItems = allItems.filter({( item : Item) -> Bool in
let categoryMatch = (item.allCat == scope) || (item.category == scope)
return categoryMatch && item.title.lowercased().contains(searchText.lowercased())
//return categoryMatch && (item.title.lowercased().range(of:searchText.lowercased()) != nil)
})
tableView.reloadData()
}
Which is fine in case of few items, but if I have large array and when using the block above, plenty unrelated items with the string typed contained would be included.
I can use predicate to get better result using BEGINSWITH, but I am failing to meet the conditions for categories. I am pretty sure the code block below isn't effective and would appreciate for more economic advise. Another obstacle is that the array contains strings with multiple words. For example:
array = ["Apple", "Apple Freshly Picked", "Apple Green", "Pear", "Melon", "Pear Yellow",....]
So the result when user starts to type "Ap" should be
Apple
Apple Green
Apple Freshly Picked
I kinda made it work without the conditions for search categories (struct) being met:
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
let words = searchText.components(separatedBy: " ")
var word1 = ""
var word2 = ""
var word3 = ""
var predicate = NSPredicate()
var array = [Item]()
if scope == "All" {
if words.count == 1{
word1 = (words[0])
predicate = NSPredicate(format: "SELF BEGINSWITH[cd] %# OR SELF LIKE[cd] %#", word1, word1)
}
else if words.count == 2{
word1 = (words[0])
word2 = (words[1])
predicate = NSPredicate(format: "SELF BEGINSWITH[cd] %# AND SELF CONTAINS[cd] %#", word1, word2)
}
else if words.count == 3{
word1 = (words[0])
word2 = (words[1])
word3 = (words[2])
predicate = NSPredicate(format: "SELF BEGINSWITH[cd] %# AND SELF CONTAINS[cd] %# AND SELF CONTAINS[cd] %#", word1, word2, word3)
}
} else {
predicate = NSPredicate(format: "title BEGINSWITH[cd] %# AND category == %#", searchText, scope)
if words.count == 1{
word1 = (words[0])
predicate = NSPredicate(format: "SELF BEGINSWITH[cd] %# OR SELF LIKE[cd] %# AND category == %#", word1, word1, scope)
}
else if words.count == 2{
word1 = (words[0])
word2 = (words[1])
predicate = NSPredicate(format: "SELF BEGINSWITH[cd] %# AND SELF CONTAINS[cd] %# AND category == %#", word1, word2, scope)
}
else if words.count == 3{
word1 = (words[0])
word2 = (words[1])
word3 = (words[2])
predicate = NSPredicate(format: "SELF BEGINSWITH[cd] %# AND SELF CONTAINS[cd] %# AND SELF CONTAINS[cd] %# AND category == %#", word1, word2, word3, scope)
}
}
array = (allItems as NSArray).filtered(using: predicate) as! [Item]
self.filteredItems = array
let lengthSort = NSSortDescriptor(key: "length", ascending: true)
let sortedArr = (self.filteredItems as NSArray).sortedArray(using: [lengthSort])
self.filteredItems = sortedArr as! [Item]
self.tableView.reloadData()
}
How do I approach this logic satisfying the categories as well as the typing string match and length/range of the string (first word) please?
Thank you
I found the way. I combined .filter and .sort to get the result:
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
let options = NSString.CompareOptions.caseInsensitive
if scope == "All"{
print("filtering All")
self.filteredItems = allItems
.filter{$0.title.range(of: searchText, options: options) != nil && $0.allCat == scope}
.sorted{ ($0.title.hasPrefix(searchText) ? 0 : 1) < ($1.title.hasPrefix(searchText) ? 0 : 1) }
}
else{
print("filtering \(scope)")
self.filteredItems = allItems
.filter{$0.title.range(of: searchText, options: options) != nil && $0.category == scope}
.sorted{ ($0.title.hasPrefix(searchText) ? 0 : 1) < ($1.title.hasPrefix(searchText) ? 0 : 1) }
}
tableView.reloadData()
}
Hope this helps somebody

Searching with NSFetchResult Controller

I've been looking on how to search using a NSFetchResultController but most post I came across are from 2 years ago. Okay I'm trying to filter the objects, which in my case are Pokemon. Here is my function I'm using.
func attemptPokemonFetch(generation: String = "1", name: String = "") {
let context = coreData.persistentContainer.viewContext
let request: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
let sortByName = NSSortDescriptor(key: "id", ascending: true)
request.sortDescriptors = [sortByName]
request.predicate = NSPredicate(format: "generation = %#", generation)
if !name.isEmpty {
request.predicate = NSPredicate(format: "name CONTAINS[cd] %#", name)
}
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
controller.delegate = self
self.controller = controller
do {
try controller.performFetch()
}
catch let err {
print(err)
}
}
I call this function in view did load, search bar did change text and when the segment changes index value. I know how to filter the Pokemon and use the searching. However the problem I come across is when I begin searching. When searching for a Pokemon in a specific generation other Pokemon from another generation will also appear. So I'll be at index 2 which is for generation 2 Pokemon and when searching, other Pokemon from different generation will be appearing. I'm not sure if its how I initialize the context. This is how my search bar function look like.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar == pokemonSearchBar {
let text = pokemonSearchBar.text!
self.attemptPokemonFetch(name: text)
collectionView.reloadData()
}
}
Another Example: Lets say I have 3 types of people some are skinny, normal and fat. When I'm at the index for skinny people I only want to search for people that are skinny not all the people. So what would I need to do to achieve this type of behavior.
Would really appreciate any help. :)
In your code
request.predicate = NSPredicate(format: "generation = %#", generation)
if !name.isEmpty {
request.predicate = NSPredicate(format: "name CONTAINS[cd] %#", name)
}
the second assignment replaces the previously assigned predicate.
What you probably want is
if name.isEmpty {
request.predicate = NSPredicate(format: "generation = %#", generation)
} else {
request.predicate = NSPredicate(format: "generation = %# AND name CONTAINS[cd] %#", generation, name)
}
A more flexible approach is to use NSCompoundPredicate:
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "generation = %#", generation))
if !name.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS[cd] %#", name))
}
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)

Search multi words in a single search iOS swift using NSPredicate

I want to search a list by typing multi words from the detailed list.
e.g,
in a claim request list there are different type of lables such as amount, requestor name, request name and nos also. So i want to search anything from this label so that i can able to find the exact request.
var predicateList = [NSPredicate]()
let words = filterText.componentsSeparatedByString(" ")
for word in words{
if count(word)==0{
continue
}
let RequestTypeArray = NSPredicate(format: "RequestType contains[c] %#", word)
let RequestEmployeeArray = NSPredicate(format: "RequestorEmployee contains[c] %#", word)
let RegesterNumberArray = NSPredicate(format: "ReqNo contains[c] %#", word)
let AmountOrDaysArray = NSPredicate(format: "AmountOrDays contains[c] %#", word)
let orCompoundPredicate = NSCompoundPredicate(type: NSCompoundPredicateType.OrPredicateType, subpredicates: [firstNamePredicate, lastNamePredicate,departmentPredicate,jobTitlePredicate])
predicateList.append(orCompoundPredicate)
}
request.predicate = NSCompoundPredicate(type: NSCompoundPredicateType.AndPredicateType, subpredicates: predicateList)
May be this answer was helpful to you..
let addresspredicate = NSPredicate(format: "address_name contains[c] %#",searchText)
let accnopredicate = NSPredicate(format: "acc_no contains[c] %#",searchText)
let propertytype = NSPredicate(format: "property_type contains[c] %#",searchText)
let subpropertytypoe = NSPredicate(format: "subproperty_type contains[c] %#",searchText)
let predicateCompound = NSCompoundPredicate.init(type: .or, subpredicates: [addresspredicate,accnopredicate,propertytype,subpropertytypoe])
filteredProperty = (propertyArray as Array).filter { predicateCompound.evaluate(with: $0) };
print("filteredProperty = ,\(filteredProperty)")

NSPredicate in CloudKit

I am having trouble in understanding some code.
let predicate = NSPredicate(format: "categoryId == %# AND position == %i", categoryId, NSUserDefaults.standardUserDefaults().objectForKey(categoryId) as! Int)
I dont know for what is used %i

Searching by object property with UISearchController?

In my updateSearchResultsForSearchController method, I have a searchPredicate that I use to find matches, however, I want to learn how to use it with an Object's property.
I have an object called Product that has a String property called title which I would like to use as my search parameter. So far, this is what my search function looks like:
func updateSearchResultsForSearchController(searchController: UISearchController) {
productSearchResults.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text)
let array = (orderGuideItemsList as NSArray).filteredArrayUsingPredicate(searchPredicate)
productSearchResults = array as! [Product]
self.tableView.reloadData()
}
where orderGuideItemsList is an array of Product objects
So is there a way for me to search based on Proudct's title property? It doesn't have to use a predicate but that's what I have from a previous tutorial.
Thanks!
You can update your predicate to
let searchPredicate = NSPredicate(format: "SELF.title CONTAINS[c] %#", searchController.searchBar.text)
This should work fine.

Resources