Why can't I search both first and last names using searchBar? - swift

I have implemented code for searchBar and it works if I either search the first or the last name of a person. For example, if I want to search Kate Bell, the search works if I write "Kate", and it works if I write "Bell". But if I write "Kate B" the search result disappear.
Here's my code:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.count == 0 {
isFiltered = false
tableViewOutlet.reloadData()
}else {
isFiltered = true
searchInternalArray = contactsInternalArray.filter({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
searchArrayGroups = sectionsArrayGroups.filter({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
searchArraySAEs = sectionsArraySAEs.filter ({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
tableView.reloadData()
}
I fond this on SO How to search by both first name and last name
It's in objective-c and I have trouble implementing it into my own code. I tried something like this:
searchInternalArray = contactsInternalArray.filter({ object -> Bool in
guard let text = searchBar.text?.range(of: object.firstName + object.lastName) else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
That's obviously not how it should be implemented though.
Edit
Didn't think it was relevant, but perhaps it is: I'm filtering multiple arrays since I have data coming from three different sources to the tableView. The whole searchBar code looks like above. It's updated.

Unless I'm missing something, there's a far simpler solution to this:
let name = firstname.lowercased() + " " + lastname.lowercased()
return name.contains(text.lowercased())
This works for any part of forename, surname, or the a part of the two with a space between. You should probably still trim leading/trailing spaces, and if you wanted to get rid of issues with variable spaces between the first and last names you could search/replace them double spaces with a single space.

Why not just create the full name from the two parts and search based on that.
let searchResults = contactsInternalArray.filter { object in
"\(object.firstName) \(object.lastName)".contains(searchText)
}
You can still lowercase them and trim if you want to.

I got idea from flanker's answer.
This works for me
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.searchBar.showsCancelButton = true
if searchText != ""{
self.isSearchActive = true
if self.object.count != 0{
filteredContacts = self. object.filter(){
//("\(($0 as FetchedContact).firstName.lowercased()) \(($0 as FetchedContact).object.lowercased()) \(($0 as FetchedContact).telephone.lowercased())")
let name = ("\(($0 as ContactModel).firstName.lowercased()) \(($0 as ContactModel).lastName.lowercased()) \(($0 as ContactModel).mobileNumber.lowercased())")
return name.contains(searchText.lowercased())
}
}
self.tblContactList.reloadData()
}
}
Which search on the base of FirsName, LastName and MobileNumber for Contacts.

Related

TableView Cell -> UISearchBar : Sort Data, where the records which have most keywords matched should be shown first and then go towards the least

Search should sort results based on a number of matched keywords. It should show the most matched keyword results on top of the list, then the rest records going in descending order. I have a UITableView wherein I have implemented UISearchBar which is going to search my tableview cells as per query inserted in the search-box. Let me now recreate a scenario to explain it. Lets say my Api fetched different key value datas and among them is a key named as "tags" which will contain a string of comma separated values as "dog, cat, pug" now whenever user enters dog into the search-box as this value is shown in one of my tableview cell it will be shown as my tags contain value dog, again if we insert second keyword as cat the above record needs to be fetched and shown to the top and after that if there exists a record which contains only dog in one of the tag values or cat as one of the tag values then it needs to be shown after it. Similarly for third word.
Query -> Cat Dog Pug
It should show on the top only those records which have all of the above keywords
After that, it should show records where any two of the above values are present
After that, it should show single single records if there exists any from above keywords.
Please help.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print("\(searchText)")
let arrSearchTexts = searchText.components(separatedBy: " ")
filteredStorage = originalStorage1.filter { (obj) -> Bool in
if let tags = obj["tags"] as? String{
var hasTag = false
for searchedTag in arrSearchTexts{
if tags.contains(searchedTag.lowercased()){
hasTag = true
break
}
}
return hasTag //tags.contains(searchText.lowercased())
}
return false
}
//Sorting data
filteredStorage.sort { (obj1, obj2) -> Bool in
if let tags1 = obj1["tags"] as? String{
var hasAllTags = true
for tagVal in arrSearchTexts{
if !tags1.contains(tagVal.lowercased()){
hasAllTags = false
break
}
}
return hasAllTags
}
return obj1.contains { (obj) -> Bool in
if let val = obj.value as? String,
let firstTag = arrSearchTexts.last{
return val.hasPrefix(firstTag)
}
return false
}
}
if searchText == "" {
filteredStorage = originalStorage1
}
tblView.reloadData()
}
for i in 0..<filteredStorage.count {
var dict = filteredStorage[i]
dict["priority"] = 0
filteredStorage[i] = dict
}
for i in 0..<filteredStorage.count{
let tagInArr : String = filteredStorage[i]["tags"] as! String
var priorityInArr : Int = 0
for tagVal in arrSearchTexts{
if tagInArr.contains(tagVal){
priorityInArr += 1
}
}
filteredStorage[i]["priority"] = priorityInArr
}
filteredStorage.sort{
((($0 as Dictionary<String, AnyObject>)["priority"] as? Int)!) > ((($1 as Dictionary<String, AnyObject>)["priority"] as? Int)!)
}

How to filter array of phrases by search term

I have a UISearchBar and I'm filtering a UICollectionView by search term like this:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredRecipes = getRecipes().filter({( recipe : Recipe ) -> Bool in
return recipe.name.lowercased().contains(searchText.lowercased())
})
if searchText == "" || searchText == " " {
filteredRecipes = getRecipes()
}
}
Each Recipe has a few properties and one of them by which I want to filter is name, that is made up of a couple of words like "Red Velvet Cake". Right now, the function would return this recipe if I searched for anything from:
red
r
lvet
ed velvet
How do I make it so that it filters the recipes based on the start of each of the words in the name. So it would only return the recipe if I searched for:
red
velvet
cake
velvet cake
And also so that it returns all recipes if the search terms is nothing, or just empty spaces?
I have looked here for answers and I can only find solutions similar to my existing one. Thank you!
Use combination of filter(_:), contains(where:) and hasPrefix(_:) like so,
filteredRecipes = getRecipes().filter({
let components = $0.name.components(separatedBy: " ")
return components.contains(where: {
$0.lowercased().hasPrefix(searchText.lowercased())
})
})
You can use enumerateSubstrings method and compare each word of recipe.name with searchText
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard !searchText.trimmingCharacters(in: .whitespaces).isEmpty else {
filteredRecipes = getRecipes()
return
}
filteredRecipes = getRecipes().filter({
var result = false
$0.name.enumerateSubstrings(in: $0.name.startIndex..<$0.name.endIndex, options: [.byWords], { (word, _, _, _) in
if let word = word, word.caseInsensitiveCompare(searchText) == .orderedSame {
result = true
}
})
return result
})
}

Search for two elements simultaneous in Swift?

I have a struct like this:
struct StructName {
var name: String?
var contents: [Content]?
}
Content looks like this:
struct Content {
var contentName: String?
}
Right now I am just searching by the names inside my array of StructName
var array = [StructName]()
func searchBar(_ searchBar: UISearchBar, textDidChange searchText:String) {
var filteredArray = array.filter { $0.name?.range(of: searchText, options: [.caseInsensitive]) != nil}
}
How would I approach this if I want to search inside the elements in contents simultaneous with name in StructName?
If I structured it like this:
let contentA = [Content(contentName: "Content_1"), Content(contentName: "Content_2")]
let contentB = [Content(contentName: "Content_1"), Content(contentName: "Content_2")]
array[0] = (StructName(name: "Name_1", contents: contentA))
array[1] = (StructName(name: "Name_2", contents: contentB))
And I wrote "NC" in my searchText, it should return everything, since there is an "N" in "Name_1" and "Name_2" and there is a "C" in "Content_1" and "Content_2".
Example of filteredArray:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredArray = array.filter{ elements in
var result = false
elements.contents.forEach{ content in
if (content.contentName.lowercased().contains(searchText.lowercased()) ) {
result = true
}
}
if (content.name?.lowercased().contains(searchText.lowercased()))!{
result = true
}
return result
}
}
Note that this does not search simultaneously as I have described.
If you want to filter and find StructNames where Content array items compares with searchBar text, when look at this code
struct Content {
var contentValue: String
}
struct StructName{
var name: String?
var contents: [Content]?
}
var array = [ StructName(name: "test1", contents: [ Content(contentValue: "contentValue1"), Content(contentValue: "contentValue2"), Content(contentValue: "searchedText") ]) ]
let searchedText = "searchedText"
let filteredArray = array.filter{ structName in
var result = false
structName.contents?.forEach{ content in
if (content.contentValue.lowercased().contains(searchedText.lowercased()) ) {
result = true
}
}
return result
}
Your usage of the word simultaneous is not clear enough to describe what sort of search result you expect.
I assume,
if (and only if) name matches searchText or any contentName in contents matches searchText, the StructName instance appears in the filteredArray.
(matches is defined similar as your name in your post.)
Then you can write something like this:
var filteredArray = array.filter {
$0.name?.range(of: searchText, options: .caseInsensitive) != nil
|| $0.contents?.contains {content in content.contentName?.range(of: searchText, options: .caseInsensitive) != nil} == true
}
If this is not what you want, you may need to specify your search condition more precisely.
(By the way, name or contents needs to be Optional?)
(For your updated example)
Seems you want character based searching with every character needs to hit.
Using a little bit modified example:
var array = [StructName]()
let contentA = [Content(contentName: "Content_1"), Content(contentName: "Content_2")]
let contentB = [Content(contentName: "Content_3"), Content(contentName: "Content_4")]
array.append(StructName(name: "Name_1", contents: contentA))
array.append(StructName(name: "Name_2", contents: contentB))
with some debugging help:
extension StructName: CustomStringConvertible {
var description: String {
return "\(name ?? ""):\(contents?.map{$0.contentName ?? ""}.joined(separator: ";") ?? "")"
}
}
print(array) //->[Name_1:Content_1;Content_2, Name_2:Content_3;Content_4]
You can write something like this:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText:String) {
let filteredArray = array.filter {element in
!searchText.lowercased().contains {searchChar in
!(element.name ?? "").lowercased().contains(searchChar)
&& !(element.contents ?? []).contains {content in (content.contentName ?? "").lowercased().contains(searchChar)}
}
}
print(filteredArray)
}
Output example:
searchBar(UISearchBar(), textDidChange: "NC") //->[Name_1:Content_1;Content_2, Name_2:Content_3;Content_4]
searchBar(UISearchBar(), textDidChange: "1") //->[Name_1:Content_1;Content_2]
searchBar(UISearchBar(), textDidChange: "2") //->[Name_1:Content_1;Content_2, Name_2:Content_3;Content_4]
searchBar(UISearchBar(), textDidChange: "12") //->[Name_1:Content_1;Content_2]
Though, I'm not sure if this is what you expect, as your description is not clear enough yet.

How to use filter in a 2D array

I have a 2D array in Swift.
let Name:[[String]] =[["India", "PA"], ["Africa", "SA"]]
I am trying to implement a search bar in the iOS app and trying to filter this 2D Array.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String, indexPath: IndexPath) {
filteredArray = Name.filter ({(names: String) -> Bool in
return names.lowercased().range(of: searchText.lowercased()) != nil
})
if searchText = ""
{
shouldShowSearchResult = true
self.tableView.reloadData()
}
else
{
shouldShowSearchResult = false
self.tableView.reloadData()
}
But I am getting a compilation error:
cannot convert value of type (String) -> Bool to expected argument
type'([String]) -> Bool'
Minimal, Complete, and Verifiable example
First of all you should always post a Minimal, Complete, and Verifiable example.
So the question does become
Given an array like this
let data = [["India", "PA"], ["Africa", "SA"]]
And given a keyword like this
let keyword = "ind"
How can I retrieve only the elements (sub-arrays) containing the keyword?
Solution
The answer is
let lowercasedKeyword = keyword.lowercased()
let filtered = data.filter {
$0[0].lowercased().range(of: lowercasedKeyword) != nil
|| $0[1].lowercased().range(of: lowercasedKeyword) != nil }
Considerations
Using and array of arrays as model is a very dangerous design choice.
Why don't you create a model like this?
struct Country {
let name: String
let code: String
}
And then build your array as shown below?
let countries = [Country(name: "India", code: "PA"), Country(name: "Africa", code: "SA")]

How to find if a string contains all the characters of a string for live searching in Swift?

I'm trying to implement search inside my app that I'm making. I have an array that I'm trying to search and I find this code online:
func filterContentForSearchText(searchText: String) {
filteredCandies = candies.filter({( candy : Candies) -> Bool in
if candy.name.lowercaseString.containsString(searchText.lowercaseString) == true {
return true
} else {
return false
}
})
tableView.reloadData()
}
The issue is that the database that I'm trying to implement search on has text that is all scrambled because it was supposed to shortened. How can I make it so that the search will check if all the letters are there instead of searching exactly the right name. Example of object from database (USDA): CRAB, DUNGINESS, RAW
If you have an answer, please make it fast enough for live searching. Non live searching makes searching terrible (at least for me)!
I'm using Swift 2.2 and Xcode 7
As an improvement to #appzYourLife's solution, you could do this with a native Swift Set, as a counted set isn't necessarily needed in this case. This will save having to map(_:) over the characters of each name and bridging them to Objective-C. You can now just use a set of Characters, as they're Hashable.
For example:
struct Candy {
let name: String
}
let candies = [Candy(name: "CRAB"), Candy(name: "DUNGINESS"), Candy(name: "RAW")]
var filteredCandies = [Candy]()
func filterContentForSearchText(searchText: String) {
let searchCharacters = Set(searchText.lowercaseString.characters)
filteredCandies = candies.filter {Set($0.name.lowercaseString.characters).isSupersetOf(searchCharacters)}
tableView.reloadData()
}
filterContentForSearchText("RA")
print(filteredCandies) // [Candy(name: "CRAB"), Candy(name: "RAW")]
filterContentForSearchText("ED")
print(filteredCandies) // Candy(name: "DUNGINESS")]
Also depending on whether you can identify this as a performance bottleneck (you should do some profiling first) – you could potentially optimise the above further by caching the sets containing the characters of your 'candy' names, saving from having to recreate them at each search (although you'll have to ensure that they're updated if you update your candies data).
When you come to search, you can then use zip(_:_:) and flatMap(_:) in order to filter out the corresponding candies.
let candies = [Candy(name: "CRAB"), Candy(name: "DUNGINESS"), Candy(name: "RAW")]
// cached sets of (lowercased) candy name characters
let candyNameCharacterSets = candies.map {Set($0.name.lowercaseString.characters)}
var filteredCandies = [Candy]()
func filterContentForSearchText(searchText: String) {
let searchCharacters = Set(searchText.lowercaseString.characters)
filteredCandies = zip(candyNameCharacterSets, candies).flatMap {$0.isSupersetOf(searchCharacters) ? $1 : nil}
tableView.reloadData()
}
First of all a block of code like this
if someCondition == true {
return true
} else {
return false
}
can also be written this ways
return someCondition
right? :)
Refactoring
So your original code would look like this
func filterContentForSearchText(searchText: String) {
filteredCandies = candies.filter { $0.name.lowercaseString.containsString(searchText.lowercaseString) }
tableView.reloadData()
}
Scrambled search
Now, given a string A, your want to know if another string B contains all the character of A right?
For this we need CountedSet which is available from Swift 3. Since you are using Swift 2.2 we'll use the old NSCountedSet but some bridging to Objective-C is needed.
Here's the code.
struct Candy {
let name: String
}
let candies = [Candy]()
var filteredCandies = [Candy]()
func filterContentForSearchText(searchText: String) {
let keywordChars = NSCountedSet(array:Array(searchText.lowercaseString.characters).map { String($0) })
filteredCandies = candies.filter {
let candyChars = NSCountedSet(array:Array($0.name.lowercaseString.characters).map { String($0) }) as Set<NSObject>
return keywordChars.isSubsetOfSet(candyChars)
}
tableView.reloadData()
}
Swift 3 code update :
func filterContentForSearchText(searchText: String, scope: String = "All") {
filteredCandies = candies.filter { candy in
return candy.name.localizedLowercase.contains(searchText.lowercased())
}
tableView.reloadData()
}