Swift - Search large array based on multiple conditions - ios

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

Related

App Crashes when searched for beyond 2 words in Search Bar in Swift 4

When I try to enter more than 2 characters in the searchbar, the app crashes. Tried changing the logic but no use. The problem might be with the whitespaces. Here is my sample code:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
tableViewAdapter.fetchRequestPredicate = nil
}
else {
var fullName = searchText.components(separatedBy: NSCharacterSet.whitespaces)
for (index, name) in fullName.enumerated() {
if name.isEmpty {
fullName.remove(at: index)
}
}
if searchText.count > 1 {
let firstName = fullName.first
let lastName = fullName[1]
tableViewAdapter.fetchRequestPredicate = NSPredicate(format: "firstName BEGINSWITH[cd] %# AND lastName BEGINSWITH[cd] %#", firstName!, lastName)
}
else if searchText.count == 1 {
let firstName = fullName.first
tableViewAdapter.fetchRequestPredicate = NSPredicate(format: "firstName CONTAINS[cd] %# AND firstName BEGINSWITH[cd] %# OR lastName BEGINSWITH[cd] %#", firstName!, firstName!, firstName!)
}
}
tableView.reloadData()
}
If you enter more than 2 characters, your program will try to access the second element of fullName, which is non-existent if you don't have a whitespace in your text.
Replace
if searchText.count > 1
else if searchText.count == 1
with
if fullName.count > 1
else if fullName.count == 1
Moreover, your code has lots of force unwrapping. You should avoid using it, especially if your data depends on user input.

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)

Swift 3 filterWithPredicate not working

Unable to filter array of dictionaries using predicate here is my code and i would like to filter array of dictionaries having key "category"
let searchText = searchTF.text?.replacingOccurrences(of: " ", with: "")
if (searchText?.characters.count)!>0 {
let pred = NSPredicate(format: "SELF beginswith[c] %#", searchTF.text!)
let array = NSMutableArray()
array.insert(dataArray as [AnyObject], at:NSIndexSet(indexesIn: NSMakeRange(0, dataArray.count)) as IndexSet)
dataArray.removeAllObjects()
getedArray = NSMutableArray(array: array.filtered(using: pred))
dataArray = getedArray.sortedArray(using: #selector(localizedCaseInsensitiveCompare(_ :)))
}
its giving error at last line i.e
getedArray = NSMutableArray(array: array.filtered(using: pred))
NSArray).sortedArrayUsingSelector(#selector(self.localizedCaseInsensitiveCompare))
its giving error at last line Use of unresolved identifier 'localizedCaseInsensitiveCompare'
Try this Swift native solution. filteredArray will contain the filtered and sorted array.
guard let searchText = searchTF.text, !searchText.isEmpty else { return }
let trimmedSearchText = searchText.replacingOccurrences(of: " ", with: "").lowercased()
let filteredArray = dataArray.filter( {($0["category"] as! String).lowercased().hasPrefix(trimmedSearchText) })
.sorted { ($0["category"] as! String).lowercased() < ($1["category"] as! String).lowercased() }
The code assumes that there is always a value for key category. Consider to use a custom struct or class to avoid the casts to String.
I have Achieved this by
let searchText = searchTF.text?.replacingOccurrences(of: " ", with: "")
if (searchText?.characters.count)!>0 {
// Put your key in predicate that is "category"
let searchPredicate = NSPredicate(format: "category CONTAINS[C] %#", searchText!)
let array = (dataArray as NSMutableArray).filtered(using: searchPredicate)
print ("array = \(array)")
if(array.count == 0){
searchTFActive = false;
} else {
searchTFActive = true;
}
self.aTable.reloadData()
}

NSPredicate Formatting Issue

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:))

NSPredicate filter array with first character

I'm trying to filter an array with the first letter of name
I'm using this :
let predicate = NSPredicate(format: "name beginswith[c] %#", sections.objectAtIndex(section) as! String)
let sectionArray = self.mContacts.filteredArrayUsingPredicate(predicate)
Where section is :
let sections = NSArray(objects: "#","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z")
# must contain all the name begin by a digit.
But I can not filter the name begin by digit with this method.
I'm looking for some advices, thanks you
I solve this with MATCHES[c]
for sectionLetter in sections {
if (sectionLetter as! String) == "#" {
let predicateFormat = NSString(format: "name MATCHES[c] '[0-9].*'")
let predicate:NSPredicate = NSPredicate(format:predicateFormat as String)
let sectionArray = self.mContacts.filteredArrayUsingPredicate(predicate)
mSectionArray.addObject(sectionArray)
} else {
let predicateFormat = NSString(format: "name MATCHES[c] '(%#).*'", sectionLetter as! String)
let predicate:NSPredicate = NSPredicate(format:predicateFormat as String)
let sectionArray = self.mContacts.filteredArrayUsingPredicate(predicate)
mSectionArray.addObject(sectionArray)
}
}

Resources