Reputation: 69
In my app I built a SearchBar which searchs for multiple words ignoring the order Search for multiple words ignoring the order in Swift 5
import Foundation
class Clinic {
var id = ""
var name = ""
var address = ""
var specialty1 = ""
var specialty2 = ""
}
In my textDidChange I have
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let searArr = searchText.lowercased().components(separatedBy: " ")
clinicsSearch = clinics.filter { item in
let lowName = item.name.lowercased()
let lowSp1 = item.specialty1.lowercased()
let lowSp2 = item.specialty2.lowercased()
let res = searArr.filter {
lowName.contains($0) ||
lowSp1.contains($0) ||
lowSp2.contains($0)
}
return !res.isEmpty
} // Credits to @Sh_Khan
With the code above I'm able to search for any typed words, order independent and that brings all entries that meet any of the criteria.
I'm now trying to build a variation from what I have above... The use case is
let's say a total of 10 clinics (First Clinic, Second Clinic, Third Clinic, Fourth Clinic and so on). Out of those, 3 are Orthopedist where clinics names are First Clinic, Second Clinic and Third Clinic. So, I can start typing Orthopedist (which would filter to 3 names). I now want the next word I type to filter the selection I have (for example, typing First would filter the 3 already shown clinics to the one(s) that meet this additional criteria). This would reduce the list to A single clinic in my example.
I've tried changing || by && which I assumed would be enough but I'm getting an unexpected behaviour. When I start typing, it considers only the first letter and then remove everything from the filter. For the example above, if I type "F" it brings First Clinic but, if I type "Fi" it brings nothing. I also thought about creating multiple filtered variables but that could limit number of typed words to the number of variables I create.
Is what I want doable?
Upvotes: 0
Views: 256
Reputation: 638
You can use a recursive search, where the matches from the first search are passed to the next search which searches only those matches for the next search term. This continues for each search term and then return only the remaining matches.
I've put it into an example which can be run from a playground so you can have a play and see how it works.
You can (and should) add an optimisation to it so the search terminates early if there are no remaining matches, but I'll leave that as an exercise for you as it will help you understand how the recursive search is working. Discuss in comments if you need help.
I also removed some temporary variables (the lowercased versions) which weren't needed, and the use of recursion means only one filter is used.
import UIKit
class Clinic: CustomStringConvertible {
var id = ""
var name = ""
var address = ""
var specialty1 = ""
var specialty2 = ""
var description: String {
get { return "Name: \(name) Sp1: \(specialty1) Sp2: \(specialty2)"}
}
init(data: [String]) {
id = data[0]
name = data[1]
address = data[2]
specialty1 = data[3]
specialty2 = data[4]
}
}
var clinics = [
Clinic(data: ["1","First Clinic","First Street","head","feet"]),
Clinic(data: ["2","Second Clinic","Second Street","legs","feet"]),
Clinic(data: ["3","Third Clinic","Third Street","head","legs"])
]
// recursive search: search remaining clinics with remaining searches
func search(searchClinics: [Clinic], searchArr: [String]) -> [Clinic] {
var searchArr = searchArr
// base case - no more searches - return clinics found
if searchArr.count == 0 {
return searchClinics
}
// iterative case - search clinic with next search term and pass results to next search
let foundClinics = searchClinics.filter { item in
item.name.lowercased().contains(searchArr[0]) ||
item.specialty1.lowercased().contains(searchArr[0]) ||
item.specialty2.lowercased().contains(searchArr[0])
}
// remove completed search and call next search
searchArr.remove(at: 0)
return search(searchClinics: foundClinics, searchArr: searchArr)
}
let searchArr = "cli le".lowercased().components(separatedBy: " ")
let foundClinics = search(searchClinics: clinics, searchArr: searchArr)
foundClinics.map() {print($0)}
// Outputs:
// Name: Second Clinic Sp1: legs Sp2: feet
// Name: Third Clinic Sp1: head Sp2: legs
Upvotes: 1
Reputation: 16327
You should precompute the lowercases. You should also probably be doing this in the background and dispatching the results back to the main queue.
Performance issues aside:
Your logic is currently if any of the words in the search term are in any of the search targets then you true.
You want to change it to if ALL of the words in the search term are in any of the search targets then return true.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let terms = searchText.lowercased().components(separatedBy: " ")
clinicsSearch = clinics.filter { item in
let lowName = item.name.lowercased()
let lowSp1 = item.specialty1.lowercased()
let lowSp2 = item.specialty2.lowercased()
let targetsContainTerm: [Bool] = searArr.map {
lowName.contains($0) ||
lowSp1.contains($0) ||
lowSp2.contains($0)
}
return targetsContainTerm.allSatisfy { $0 = true}
} // Credits to @Sh_Khan
Upvotes: 1