I have a very large array of data that I use a searchbar to filter. However I would like to filter only exactly that which the user enters in the searchbar. I.e. if you type "b" you only get items starting with b. The list is more than 200,000 items so now even with many letters entered the results showing are often irrelevant. I have tried searching a lot but every guide I have found is a variation of what I have :
extension MapTableController : UISearchResultsUpdating {
func filterContentForSearchText(_ searchText: String) {
if(searchText == "") {
matchingItems = []
}
else {
matchingItems = arrayOfFixes.filter { fix in
return fix.lowercased().contains(searchText.lowercased())
}
tableView.reloadData()
}
}
func updateSearchResults(for searchController: UISearchController) {
let searchText = searchController.searchBar.text
filterContentForSearchText(searchText!)
self.tableView.reloadData()
}
}
I understand that using "contains" will have the above mentioned effect. But I can't figure out how to filter it in the desired way. For example I would like it so that if I enter "Ben" it will show results like "Bendi", "Benhu", "Benji" etc. Not so that it shows results like "Juben", "Ibeno" etc, you get the picture.
Thanks for any help!
You can use 'hasPrefix' instead of 'contains' for your filter:
return fix.lowercased().hasPrefix(searchText.lowercased())
Related
In my app I have a search bar where people can add free text and get the results the meet the criteria
func search(searchClinics: [Clinica], searchArr: [String]) -> [Clinica] {
var searchArr = searchArr
// base case - no more searches - return clinics found
if searchArr.count == 0 {
return searchClinics
}
// itterative case - search clinic with next search term and pass results to next search
let foundClinics = searchClinics.filter { item in
(item.name.lowercased() as AnyObject).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)
}
I also have a flag to identify if the searchBar is being used
var searching = false // Checks if searchBar was used
In case searchBar was used, it returns the filtered array data otherwise returns the full list. For example
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
let totalClinics = clinicsSearch.count
if totalClinics == 0 {
return 1
} else {
return totalClinics
}
} else {
let totalClinics = clinics.count
if totalClinics == 0 {
return 1
} else {
return totalClinics
}
}
}
I'm now willing to add another viewController where the user would be able to define specific filters (like State, City, Specialty among others). Once the user clicks apply, it would go back to the previous viewController with filtering data.
I'm in doubt with the best approach to apply this filter. At first I though about doing something like:
User clicks a button in the navigationBar and opens "Search" viewController;
User inputs Data;
User clicks "Apply";
Call previous viewController;
I would add another status flag with status "True" that will be used in my tableView ViewController. If true, considers the list of clinics with filters applied. If not, show the full list of clinics;
I've a lot of searching in stackoverflow but I found a lot of filtering/searchbar stuff but none related to a separate search/filter viewController.
Would you recommend this approach or is there a better way to do it?
One of my concerns here if with step 4... if I call a segue, wouldn't I be stacking views and consuming more memory?
Thanks
In my app user click filter button present Controller open
let vc = storyboard?.instantiateViewController(withIdentifier: "searchFilter") as! SearchFilter
vc.modalPresentationStyle = .overCurrentContext
vc.SearchCompletion = {(model,flag) in
if(flag){
self.serachArr.removeAllObjects()
for i in 0..<model.count{
self.serachArr.add(ListModel(data: model[i] as! NSDictionary))
}
self.ListTable.reloadData()
}
}
self.present(vc, animated: true, completion: nil)
User click apply button and pass data previous Viewcontroller
self.SearchCompletion(dataArr,true)
I have a UITableView subclass that I am populating from a Firestore collection. I only grab 20 documents at a time, and use the UITableViewDataSourcePrefetching delegate to get the next "page" of 20 documents from Firestore when the user nears the end of the loaded array.
The following is my code that almost perfectly achieves this, omitting/obfuscating where appropriate.
class MyCustomViewController: UIViewController {
#IBOutlet weak var myCustomTableView: MyCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
myCustomTableView.query = Firestore.firestore().collection("MyCollection").whereField("foo", isEqualTo: "bar").order(by: "timestamp", descending: true)
myCustomTableView.fetchNextPage()
}
}
extension MyCustomViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard let tableView = tableView as? MyCustomTableView else {
return
}
tableView.prefetch(indexPaths: indexPaths)
}
}
class MyCustomTableView: UITableView {
var documents = [DocumentSnapshot]()
var query: Query?
let querySize = 20
private var fetchSemaphore = DispatchSemaphore(value: 1)
public func prefetch(indexPaths: [IndexPath]) {
for indexPath in indexPaths {
if indexPath.section >= documents.count - 1 {
fetchNextPage()
return
}
}
}
public func fetchNextPage() {
guard let query = query else {
return
}
guard documents.count % querySize == 0 else {
print("No further pages to fetch.")
return
}
guard fetchSemaphore.wait(timeout: .now()) == .success else { return }
if self.documents.isEmpty {
query.limit(to: querySize).addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
self.documents.append(contentsOf: snapshot.documents)
self.reloadData()
self.fetchSemaphore.signal()
}
}
else {
// I think the problem is on this next line
query.limit(to: querySize).start(afterDocument: documents.last!).addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
for document in snapshot.documents {
if let index = self.documents.firstIndex(where: { $0.documentID == document.documentID }) {
self.documents[index] = document
}
else {
self.documents.append(document)
}
}
self.reloadData()
self.fetchSemaphore.signal()
}
}
}
}
I think I know what/where the problem is (see comment in fetchNextPage()), but I may be wrong. Even if I am correct, I have not been able to come up with a way to fix it.
My query is sorted, in descending order, by the documents' timestamp value which, as the name suggests, represents the time at which the document was created. This means that, in the table view, the newest documents will appear at the top, and the oldest at the bottom.
Everything works great, except for...
The problem: When a new document is created, every item in the table gets shunted down a row because the new document has the newest timestamp and gets placed at the top. Great, except that when this happens, all of the query snapshot listeners (except for the first one) are no longer using the correct document to start after. The document snapshot originally retrieved with documents.last! is no longer the correct document that the query should start after. I do not know how to change the startAfter parameter of an existing snapshot listener. I could not find any method belonging to the query that I could use to alter this.
Again, this might not actually be the reason I'm seeing things out of order when new documents are added, but I think it is. If anyone has advice for how to resolve this I'd greatly appreciate it.
References I have used to get here:
https://firebase.google.com/docs/firestore/query-data/query-cursors
https://www.raywenderlich.com/5786-uitableview-infinite-scrolling-tutorial
Additional notes:
Even though it'd be much easier and solve this problem, I do not want to use getDocuments on the query. Having the near-real-time updates is important in this case.
I cannot use FirebaseUI because my use-case requires functionality not yet available. For example, you may have noticed that I used indexPath.section instead of indexPath.row, which is because the table is made up of many single-rowed sections so that vertical padding can be put between cells.
I just implemented a filtering mechanism in my UICollectionView and I think this is not the correct methodology I should have chosen.
How can I improve my code to make it have better performance?
I have a screen where you input your criterias for filtering, and then this code handles the rest:
extension ProductsCollectionViewController
{
#objc func Filter()
{
performSegue(withIdentifier: "segue_to_filter_view", sender: self)
}
#IBAction func unwindFromFilterScreenAccept(segue: UIStoryboardSegue)
{
if segue.source is FilterViewController
{
if let senderVC = segue.source as? FilterViewController
{
if senderVC.chosenCategory != "" {categoryFilter = senderVC.chosenCategory}
self.areFiltersSet = true
}
}
self.tabBarController?.tabBar.isHidden = false
self.products.removeAll()
self.LoadProducts(productsToShow: pageType)
//self.LoadProducts(productsToShow: pageType)
}
#IBAction func unwindFromClearAllFilters(segue: UIStoryboardSegue)
{
ClearAllFilters()
self.products.removeAll()
self.LoadProducts(productsToShow: pageType)
}
private func ClearAllFilters()
{
areFiltersSet = false
}
private func FilterProduct(prod: Product) -> Bool
{
if areFiltersSet == false {return true}
return FilterProductByCategory(prod:prod) && FilterProductByLocation(prod:prod) && FilterProductByCondition(prod:prod)
}
Also:
I have a search bar implemented. Will the two work together? Is there a way to properly integrate the two?
P.S:
The Filtering itself is checked when adding a product to the collection when loading products.
Filtering by performing a segue is not the best way. It will be better to update the collectionView itself depending on the filter that you choose. For example, if you have products that correspond to a specific category you can just do something like:
let products = [Product(category: "one"), Product(category: "one"), Product(category: "two")]
let foo = products.filter {$0.category == "one"}
that filter function will return only the products that correspond to category one. Then you can just use the filtered array to populate the collectionView.
About the searchBar, yes, they will work together with any other filter function. Basically you will just perform two filters:
Based on the category property
Based on the substring that you input
Just for future reference, if you add the searchBar as collectionViewCell or as header you will HAVE to reload items with the indexPaths because reloadData() will resign the responder from the searchBar.
One more thing, just as a suggestion, it is a common practice to name your methods with lowercase -> filter() instead of Filter().
I have a search bar that filters through an xml array of recipe titles. The problem is that I have to search the entire title, otherwise I don't see suggested results. For example, if I have "Whole Grain Waffles" and "Whole Wheat Bread" typing "Whole" returns nothing. Typing "Whole Grain Waffles" returns successfully. This is the searchBar function
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text == nil || searchBar.text == "" {
isSearching = false
view.endEditing(true)
myTableView.reloadData()
} else {
isSearching = true
filteredData = tableViewDataSource.filter({$0.title == searchBar.text})
myTableView.reloadData()
}
}
I'm pretty sure the solution has to do with case sensitivity, and returning certain characters when setting the filteredData. Thanks for help in advance
You could use contains to filter any item in array that contains the text or you can also use hasPrefix, if you want to search for the strings that start with the search text.
Something like this,
filteredData = tableViewDataSource.filter { $0.title.contains(searchBar.text) ?? "" }
Or,
filteredData = tableViewDataSource.filter { $0.title.hasPrefix(searchBar.text) ?? "" }
Using range worked in my situation
filteredData = tableViewDataSource.filter({$0.title.range(of: searchBar.text!) != nil})
I have a tableview as shown in the picture below:
I want the filtering to happen based on both the labels in each cell of the Tableview. The result must be in the form of 2 labels .ie:if user searches for N it must displa NewYork ,Ny and print in the first cell ,Los AngelesCA and Nishat in the second and so on.Filter must filter both the arrays.Can we filter 2 arrays like in the below code:
func updateSearchResultsForSearchController(searchController: UISearchController) {
if let searchText = searchController.searchBar.text {
filteredData = data.filter({(dataString: String) -> Bool in
return dataString.rangeOfString(searchText, options: .CaseInsensitiveSearch) != nil
})
tableView.reloadData()
}
}
Can we pass 2 arrays to the filter? Or what method should I use to do the same?