André Fernandes
André Fernandes

Reputation: 69

Multiple filters for the same searchBar and ignoring the oder in Swift 5

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

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

Answers (2)

Najinsky
Najinsky

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

Josh Homann
Josh Homann

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

Related Questions