How to search for object in Dictionary with SearchBar TableView? - ios

I am using a Dictionary for TableView. I also have a search bar for searching an item in the table.
The data is stored as following:
var namesDic = [String: [Name]]()
And I search through it as:
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text! == "" {
filteredNames = namesDic
} else {
filteredNames.removeAll()
for persons in namesDic {
let person = persons.value.filter { $0.getName().contains(searchController.searchBar.text!)
}
for boy in person {
filteredNames[String(describing: boy.getName().characters.first!)] = [boy]
}
}
}
self.tableView.reloadData()
}
Problem: I only get one record now when I search for an item.
Is there a better way to implement above?

More lines of code but it is working now. I stored values from dictionary in different variable and filtered those. The filtered values stored in the another variable and then iterate through.
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text! == "" {
filteredNames = namesDic
} else {
filteredNames.removeAll()
var namesArray = [Name]()
for (_, value) in namesDic {
namesArray += value
}
let namesFilteredArray = namesArray.filter { $0.getName().lowercased().contains(searchController.searchBar.text!.lowercased())
}
for filteredName in namesFilteredArray {
if let letter = filteredName.getName().characters.first {
if filteredNames[String(letter)] != nil {
filteredNames[String(letter)]?.append(filteredName)
} else {
filteredNames[String(letter)] = [filteredName]
}
}
}
}
self.tableView.reloadData()
}

Related

How to filter data in searchController using Share Class in swift 4

I am using UISearchController to filter data. Here is my code:
var filteredData: [ShareObj] = [ShareObj]()
var arrHotelData : [ShareObj] = [ShareObj]()
extension AddAddressVC : UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
if let searchText = searchController.searchBar.text {
filteredData.removeAll()
let type2Array = arrHotelData.filter { $0.strHotelName == searchText.lowercased()}
print(type2Array)
tableView.reloadData()
}
}
}
Here is my code to save data
for i in 0..<arrData.count
{
let ShareObj : ShareObj = AddHotelShareObj()
let dict = arrData[i]
if (dict[""]) != nil {
ShareObj.strHotelName = dict[""] as! String
}
if (dict[""]) != nil {
ShareObj.strAddress = dict[""] as! String
}
if (dict[""]) != nil {
ShareObj.strZipCode = dict[""] as! String
}
self.arrHotelData.append(ShareObj)
But array is returning []. Can you help me why it's not working
Try this:
func updateSearchResults(for searchController: UISearchController) {
if let searchText = searchController.searchBar.text {
filteredData.removeAll()
let filterPredicate = NSPredicate(format: "self contains[c] %#", argumentArray: [searchText])
let type2Array = self.arrHotelData.filter { filterPredicate.evaluate(with: $0.strHotelName) }
print(type2Array)
tableView.reloadData()
}
}
you can try this:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredArray.removeAll()
filteredArray = arrHotelData.filter({$0.lowercased().contains(searchText.lowercased())})
self.tableView.reloadData()
}
Here you should use filteredData to display and replace the code
let ShareObj : ShareObj = AddHotelShareObj() to
let shareObj : ShareObj = AddHotelShareObj() because same reference and Type may cause problem.

Firebase Swift - Realtime update does not work when result is filtered by search controller

I am making an iOS app using Swift and Firebase. The view controller retrieves/observes items and stores them in items array. Item cell has + and - buttons that perform an update on the quantity of the item using its indexPath.
If I use search controller to filter items, I get to store search results in filteredItems array. I am trying to update quantity of these filtered items but they only get to update once when I tap + or - button and does not show the update in search result view (no separate view, I display the filteredItems using data source in the same view). Even if I hit it multiple times, it always updates once.
Once I go back to the regular view by canceling search bar, I see 1 up or down depends on which button I tapped. Does anyone know what might be causing the problem here?
class ItemsViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, SortTypeTableViewControllerDelegate, ItemListCellDelegate {
private var items = [Item]()
private var filteredItems = [Item]()
private func retrieveFirebaseData(sortType: ItemSort.SortType, sortOrder: ItemSort.SortOrder) {
guard let currentUser = Auth.auth().currentUser else {
return print("user not logged in")
}
let itemsRef = DatabaseReferenceHelper.usersRef.child(currentUser.uid).child("items")
itemsRef.queryOrdered(byChild: sortType.rawValue).observe(.value) { (snapshot) in
var newItems: [Item] = []
for item in snapshot.children {
let item = Item(snapshot: item as! DataSnapshot)
if self.displayFavoritesOnly == true {
if item.favorite == true {
newItems.append(item)
}
} else {
newItems.append(item)
}
}
self.items = sortOrder == .ascending ? newItems : newItems.reversed()
self.collectionView.reloadData()
}
}
// this is from item cell delegate
func increaseDecreaseQuantity(_ sender: ItemListCell, increment: Bool) {
guard let tappedIndexPath = collectionView.indexPath(for: sender) else {
return
}
let item: Item
item = isFiltering() ? filteredItems[tappedIndexPath.item] : items[tappedIndexPath.item]
let updatedQuantity = increment == true ? item.quantity + 1 : item.quantity - 1
guard let currentUser = Auth.auth().currentUser else {
return print("user not logged in")
}
let itemsRef = DatabaseReferenceHelper.usersRef.child(currentUser.uid).child("items")
itemsRef.child(item.key).updateChildValues(["quantity": updatedQuantity])
}
// Here's the search logic I learned from Ray Wenderlich
private func searchBarIsEmpty() -> Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
private func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredItems = items.filter({$0.title.lowercased().contains(searchText.lowercased())})
collectionView.reloadData()
}
private func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
extension ItemsViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
Reload your collectionView after each + & -. Once you reload your collection view, new updates will be visible.
I fixed the issue. All I did was adding another parameter called "searchText" in retrieveFirebaseData function and use the function in filterContentForSearchText.
Basically, I needed to filter items UNDER observe method like in the code below.
private func retrieveFirebaseData(sortType: ItemSort.SortType, sortOrder: ItemSort.SortOrder, searchText: String?) {
guard let currentUser = Auth.auth().currentUser else {
return print("user not logged in")
}
let itemsRef = DatabaseReferenceHelper.usersRef.child(currentUser.uid).child("items")
itemsRef.queryOrdered(byChild: sortType.rawValue).observe(.value) { (snapshot) in
if let searchText = searchText {
// FILTER HERE
self.filteredItems = self.items.filter({$0.title.lowercased().contains(searchText.lowercased())})
} else {
var newItems: [Item] = []
for item in snapshot.children {
let item = Item(snapshot: item as! DataSnapshot)
if self.displayFavoritesOnly == true {
if item.favorite == true {
newItems.append(item)
}
} else {
newItems.append(item)
}
}
self.items = sortOrder == .ascending ? newItems : newItems.reversed()
}
self.collectionView.reloadData()
}
}
private func filterContentForSearchText(_ searchText: String, scope: String = "All") {
retrieveFirebaseData(sortType: itemSortType, sortOrder: itemSortOrder, searchText: searchText)
}
"filterContentForSearchText" function used to filter items and reload table like this:
private func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredItems = items.filter({$0.title.lowercased().contains(searchText.lowercased())})
collectionView.reloadData()
}

Firebase and UISearchbarController searching via the server and not the client -Swift iOS

Does anyone have any info on how to incorporate Firebase into a UISearchController delegate? I can't find any solid info on it. There may possibly be thousands of employees.
I know how to use the search controller delegates updateSearchResultsForSearchController and using a NSPredicate to filter what I'm looking for if I was using NSUserDefaults but using Firebase I'm uncertain.
I've added some more code to my question
I have a custom data model object saved in FirebaseDatabase and I'd like to search on all of the following properties within the object.
lastName
idNumber
deptNumber
position
Searching any of these properties should first show a partial string inside the table cells until the entire string i'm looking for is shown. So if I typed in the letter "S" then all employee last names beginning with "S" should show. If I enter "Sa" the in would filter to those letters". From my understanding I should use "\u{f8ff}" to get the partial search string but no data is returned.
Anyhow here's all the code
My object is:
class Employee{
var firstName: String?
var lastName: String?
var idNumber: String?
var deptNumber: String?
var position: String?
}
My paths
-root
-users
-uid
|_"email":"emailAddress"
|_"userID":"uid"
|_"firstName":"firstName"
|_"lastName":"lastName"
-employees
-hireDate
-uid //this is the same uid from the users node so I know who's who
|_"firstName":"firstName"
|_"lastName":"lastName"
|_"idNum":"idNumber"
|_"deptNumber":"deptNumber"
|_"position":"position"
My rules:
What's happening here is the day an employee is hired they are asked to create a company account using their email address and pw.
At the same time a "employees" path is created with a child being a "hireDate" path and finally the employees "uid" path. This employee "uid" is the path I want to search on from the "employees" node
{
"rules": {
"users" : {
"$uid" : {
".read": true,
".write": "auth != null && auth.uid == $uid"
}
},
"employees": {
"$hireDate": {
"$uid": {
".read": true,
".indexOn": ["lastName", "idNumber", "deptNumber", "position"]
}
}
}
}
}
My searchController
import UIKit
class SearchController: UIViewController{
#IBOutlet var tableView: UITableView!
var searchController: UISearchController!
var employeesToFilter = [Employee]()
var filteredSearchResults = [Employee]()
override func viewDidLoad() {
super.viewDidLoad()
self.searchController = UISearchController(searchResultsController: nil)
self.tableView.delegate = self
//all searchController properties get set here no need to include them though
let ref = FIRDatabase.database().reference()
let employeeRef = ref.child("employees")
employeeRef?.queryOrderedByChild("lastName").queryStartingAtValue("\u{f8ff}").queryLimitedToFirst(20).observeEventType(.ChildAdded, withBlock: {
(snapshot) in
if let dict = snapshot.value as? [String:AnyObject]{
let firstName = dict["firstName"] as! String
let lastName = dict["lastName"] as! String
let idNumber = dict["idNumber"] as! String
let deptNumber = dict["deptNumber"] as! String
let position = dict["position"] as! String
let employee = Employee()
employee.firstName = firstName
employee.lastName = lastName
employee.idNumber = idNumber
employee.deptNumber = deptNumber
employee.position = position
self.employeesToFilter.append(employee)
}
})
self.tableView.reloadData()
}
override func viewDidAppear(animated: Bool) {
self.searchController.active = true
}
deinit {
self.searchController = nil
}
}
//MARK:- TableView Datasource
extension SearchBuildingController: UITableViewDataSource, UITableViewDelegate{
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.filteredSearchResults.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("SearchCell", forIndexPath: indexPath) as! SearchCell
let searchString = self.filteredSearchResults[indexPath.row]
cell.firstNameLabel.text = searchString.firstName
cell.lastNameLabel.text = searchString.lastName
cell.idNumberLabel.text = searchString.idNumber
cell.deptNumberLabel.text = searchString.deptNumber
cell.positionLabel.text = searchString.position
return cell
}
}
//MARK:- SearchController Delegates
extension SearchController: UISearchResultsUpdating, UISearchBarDelegate, UISearchControllerDelegate{
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
tableView.reloadData()
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.employeesToFilter.removeAll(keepCapacity: false)
self.filteredSearchResults.removeAll(keepCapacity: false)
let searchText = self.searchController.searchBar.text
let searchPredicate = NSPredicate(format: SELF.lastName CONTAINS [c] %# OR SELF.idNumber CONTAINS [c] %# OR SELF.deptNumber CONTAINS[c] %# OR SELF.position CONTAINS [c] %#", searchText!, searchText!, searchText!, searchText!)
let array = (self.employeesToFilter as NSArray).filteredArrayUsingPredicate(searchPredicate)
self.filteredSearchResults = array as! [Employee]
tableView.reloadData()
}
}
Here is an example of how I have accomplished this using Firebase building a list of campuses. This method loads all of the data that is in the table view up front making it easy to search and filter.
My campus object is pretty simple with an id and a name.
struct Campus {
var id: String
var name: String
}
In the view controller I have two arrays. One is to hold the list of all campuses returned and the other array is for the filtered campuses.
let campuses = [Campus]()
let filteredCampuses = [Campus]()
I then called a method that I had set up to load the campuses from Firebase.
override func viewDidLoad() {
...
getAllCampusesFromFirebase() { (campuses) in
self.campuses = campuses
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
}
Then when performing the search I filter out the campuses comparing the campus name to the search text from the search bar.
func updateSearchResultsForSearchController(searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else {
return
}
filteredCampuses = campuses.filter { campus in
return campus.name.lowercaseString.containsString(searchText.lowercaseString)
}
tableView.reloadData()
}
If you are not loading all of the data up front then Firebase provides some handy methods to call that you can use to filter the data based on the reference path. https://firebase.google.com/docs/database/ios/lists-of-data
queryStarting(atValue) or queryStarting(atValue:childKey:) would probably be the one that you'd want to use in this case.
ref.queryStarting(atValue: Any?)
ref.queryStarting(atValue: Any?, childKey: String?)

how to check all fields in the array contains data

I want to check the array to see if all the fields have a value, and if all the fields have a value, then I want it to do something. My code does work but it is really messy. I'd like to know if there is an easier way of doing this.
#IBAction func testBtn(sender: AnyObject) {
self.textData = arrayCellsTextFields.valueForKey("text") as! [String]
for item in textData {
if item.isEmpty {
print("Missing")
switchKey = true
// alertviewer will go here
break
} else {
switchKey = false
}
}
if switchKey == false {
//navigation here
print("done")
}
}
You can do this with the filterfunction
if textData.filter({$0.isEmpty}).count > 0 {
// there is at least one empty item
} else {
// all items contain data
}
Try this combination of guard and .filter:
#IBAction func testBtn(sender: AnyObject) {
self.textData = arrayCellsTextFields.valueForKey("text") as! [String]
guard textData.count == (textData.filter { !$0.isEmpty }).count else {
print("Missing")
return
}
print("Done")
}

How to Segue from a table filled with results from search controller

The old UISearchDisplayController class is now deprecated and instead we have to use the new UISearchController. There used to be a property in the old class called "SearchResultsTableView" but it's gone from the new class.
I populate a table with data and all works as intended - including segueing each row's details to another scene. I throw a search bar in there (programmatically - using the new searchController) and it successfully reloads the original table with any found results.
HOWEVER, when touching a selected row after a search, the segue passed along is that of the original table row that happens to be in the same position of the one touched now! (i.e. if I choose the current second row of a search, the next scene will segue the details of the second row of the original table!) That's because despite the data in the rows are being successfuly repopulated with the search data, the index numbers are still those of the old data.
It used to be with the old type that we would check this as such:
if (self.resultSearchController.active) {
let indexPath = self.searchDisplayController!.searchResultsTableView.indexPathForSelectedRow()
} else {
let indexPath = self.tableView.indexPathForSelectedRow()
So I think that with the old UISearchDisplayController class you actually got a new table, whereas with the new SearchController Class you only get new rows inside the old table? This totaly doesn't make sense !
Here is my full code per request:
import UIKit
import Foundation
class secondTableViewController: UITableViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating {
var filteredTableData = [String]()
var resultSearchController = UISearchController()
//these 2 are standard for the title and subtitle
var TableTitle:Array< String > = Array < String >()
var TableSub:Array< String > = Array < String >()
//the following are for my seque to next scene
var the_fname:Array< String > = Array < String >()
var the_basics:Array< String > = Array < String >()
var the_p_method:Array< String > = Array < String >()
var the_seats:Array< String > = Array < String >()
var the_notes:Array< String > = Array < String >()
var the_tableData:Array< String > = Array < String >()
override func viewDidLoad() {
tableView.delegate = self
tableView.dataSource = self
self.title = currentBus
super.viewDidLoad()
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
self.tableView.tableHeaderView = controller.searchBar
return controller
})()
// Reload the table
self.tableView.reloadData()
var url = "http://the_path_to_my_json_file"
get_data_from_url(url)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// 2
if (self.resultSearchController.active) {
return self.filteredTableData.count
}
else {
return TableTitle.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("secondtableCell", forIndexPath: indexPath) as! UITableViewCell
// Configure the cell...
if (self.resultSearchController.active) {
cell.textLabel?.text = filteredTableData[indexPath.row]
//cell.detailTextLabel?.text = TableSub[indexPath.row]
}else{
cell.textLabel?.text = TableTitle[indexPath.row]
cell.detailTextLabel?.text = TableSub[indexPath.row]
}
return cell
}
func get_data_from_url(url:String)
{
let httpMethod = "GET"
let timeout = 15
let url = NSURL(string: url)
let urlRequest = NSMutableURLRequest(URL: url!,
cachePolicy: .ReloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 15.0)
let queue = NSOperationQueue()
NSURLConnection.sendAsynchronousRequest(
urlRequest,
queue: queue,
completionHandler: {(response: NSURLResponse!,
data: NSData!,
error: NSError!) in
if data.length > 0 && error == nil{
let json = NSString(data: data, encoding: NSASCIIStringEncoding)
self.extract_json(json!)
}else if data.length == 0 && error == nil{
println("Nothing was downloaded")
} else if error != nil{
println("Error happened = \(error)")
}
}
)
}
func extract_json(data:NSString)
{
var parseError: NSError?
let jsonData:NSData = data.dataUsingEncoding(NSASCIIStringEncoding)!
let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &parseError)
if (parseError == nil)
{
if let my_pass_list = json as? NSArray
{
for (var i = 0; i < my_pass_list.count ; i++ )
{
if let each_pass = my_pass_list[i] as? NSDictionary
{
if let fname = each_pass["fname"] as? String
{
if let lname = each_pass["lname"] as? String
{
if let numofseats = each_pass["numofseats"] as? String
{
if let showed_up = each_pass["showed_up"] as? String
{
if let res_id = each_pass["resnum"] as? String
{
if let res_notes = each_pass["res_notes"] as? String
{
if let payment_description = each_pass["payment_description"] as? String
{
// the_tableData.append(fname)
the_fname.append(fname)
the_basics.append(fname + " " + lname)
the_p_method.append(payment_description)
the_seats.append(numofseats)
the_notes.append(res_notes)
TableTitle.append(fname + " " + lname)
TableSub.append("Seats Reserved: " + numofseats + ". Showed Up: " + showed_up + ". Notes:" + res_notes)
the_tableData = TableTitle
}
}
}
}
}
}
}
}
}
}
}
do_table_refresh();
}
func do_table_refresh()
{
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
return
})
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using [segue destinationViewController].
var thirdScene = segue.destinationViewController as! customer_details_View_Controller
if let indexPath = self.tableView.indexPathForSelectedRow() {
/*
so what I'm missing is to be able to check
if (self.resultSearchController.active) {
and if yes have indexPath be the self.resultSearchController.resultSearchTableView.indexPathForSelectedRow() {
or something of that nature
*/
thirdScene.dotrav = todayString
thirdScene.from = currentBus
thirdScene.basics = the_basics[indexPath.row]
thirdScene.p_method = the_basics[indexPath.row]
thirdScene.seats = the_tableData[indexPath.row]
thirdScene.notes = the_notes[indexPath.row]
}
// Pass the selected object to the new view controller.
}
func updateSearchResultsForSearchController(searchController: UISearchController)
{
filteredTableData.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text)
let array = (the_tableData as NSArray).filteredArrayUsingPredicate(searchPredicate)
filteredTableData = array as! [String]
self.tableView.reloadData()
}
}
You need to account for the fact that you are going to have different data in your tableView depending on the search result. You can still use self.tableView.indexPathForSelectedRow.
What I do, is keep a reference to my base data, and then keep a reference to my filtered data, and display my filtered data in the tableView at all times. If my searchBar has no text, then my filtered data is equal to my base data.
Example:
class MyTableViewController: UITableViewController, UISearchResultsUpdating {
var data: [String] = ["One", "Two", "Three", "Four", "Five"]
var filteredData: [String]!
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
setUpSearchController()
setFilteredDataForCurrentSearch()
}
private func setUpSearchController() {
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
self.tableView.tableHeaderView = searchController.searchBar
}
private func setFilteredDataForCurrentSearch() {
if let searchString = searchController.searchBar.text where !searchString.isEmpty {
filteredData = data.filter({ (string: String) -> Bool in
return searchString.rangeOfString(string, options: NSStringCompareOptions.CaseInsensitiveSearch) != nil
})
} else {
filteredData = data
}
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
setFilteredDataForCurrentSearch()
}
}
Now, you can implement all of your UITableViewDataSource and UITableViewDelegate methods using the filteredData.
In prepareForSegue, you retrieve the correct selected object like:
let indexPath = tableView.indexPathForSelectedRow()
let selectedObject = filteredData[indexPath.row]

Resources