UISearchController Filter Swift - ios

I have a UITableViewCell with 3 subviews that I would like to filter when I search, as seen here:
1.The image view
2.The name label (black text)
3.The street name label (blue text)
This is what I've done so far, I've only managed to understand how to filter 1 array which is the name:
MainTableView.swift
var FilteredNames = [String]
func updateSearchResultsForSearchController(searchController:UISearchController) {
// Filter Names
self.filteredNames = self.names.filter { (name:String) -> Bool in
if name.lowercaseString.containsString(self.searchController.searchBar.text!.lowercaseString){
return true
} else {
return false
}
}
self.resultsController.tableView.reloadData()
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 100.5
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Search
if tableView == self.tableView {
self.streets.count
return self.names.count
} else {
self.filteredStreets.count
return self.filteredNames.count
}
// return names.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = self.tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
if tableView == self.tableView {
cell.photo.image = self.images[indexPath.row]
cell.name.text = names[indexPath.row]
cell.streetName.text = streets[indexPath.row]
} else {
cell.photo.image = self.images[indexPath.row]
cell.name.text = self.filteredNames[indexPath.row]
cell.streetName.text = self.streets[indexPath.row]
}
return cell
}
As of right now, when I search, the image view & the street label are not synchronized with the name. I want to filter all 3 subviews to synchronize correctly. How can I achieve this?
I understand i need to use a struct and filter all 3 with one object but i'v encountered some difficulties managing to do that any help will be appreciated thank you !
Edit:here is my code right now:
override func viewDidLoad() {
super.viewDidLoad()
var searchController : UISearchController!
var resultsController = UITableViewController()
// //
definesPresentationContext = true
self.resultsController.tableView.dataSource = self
self.resultsController.tableView.delegate = self
self.searchController = UISearchController(searchResultsController: self.resultsController)
self.tableView.tableHeaderView = self.searchController.searchBar
self.searchController.searchResultsUpdater = self
self.searchController.dimsBackgroundDuringPresentation = true
self.searchController.searchBar.sizeToFit()
self.searchController.searchBar.barTintColor = UIColor.blackColor()
self.searchController.searchBar.endEditing(true)
self.searchController.searchBar.placeholder = "חפש ברים"
allUsers = createUsers(names: names, streets: streets, images: images)
filteredUsers = allUsers
}
var allUsers: [User]!
var filteredUsers: [User]!
func createUsers(names names: [String], streets: [String], images: [UIImage?]) -> [User] {
var users = [User]()
guard names.count == streets.count && names.count == images.count else { return users }
for (index, name) in names.enumerate() {
let user = User(name: name, streetName: streets[index], image: images[index])
users.append(user)
}
return users
}
//MARK : Search !
func updateSearchResultsForSearchController(searchController:UISearchController) {
if let searchText = searchController.searchBar.text?.lowercaseString {
if searchText.characters.count == 0 {
filteredUsers = allUsers
}
else {
filteredUsers = allUsers.filter {
return $0.name.lowercaseString.containsString(searchText) ||
$0.streetName.lowercaseString.containsString(searchText)
}
}
}
self.resultsController.tableView.reloadData()
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
return 100.5;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
////////
////////
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredUsers.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = self.tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
let user = filteredUsers[indexPath.row]
cell.photo.image = user.image
cell.name.text = user.name
cell.streetName.text = user.streetName
return cell
}

I would strongly recommend to have only one dataSource instead of separate arrays for names, street names and images. In the following code filteredUsers is always used as dataSource and allUsers is just a stored full array, which is used for updating filteredUsers every time a new text is entered in search bar.
Create a model:
struct User {
var name: String
var streetName: String
var image: UIImage?
}
ViewController:
var allUsers: [User]!
var filteredUsers: [User]!
override func viewDidLoad() {
super.viewDidLoad()
//assuming you already have three arrays with the same amount of elements in each:
allUsers = createUsers(names: names, streets: streets, images: images)
filteredUsers = allUsers
}
func createUsers(names names: [String], streets: [String], images: [UIImage?]) -> [User] {
var users = [User]()
guard names.count == streets.count && names.count == images.count else { return users }
for (index, name) in names.enumerate() {
let user = User(name: name, streetName: streets[index], image: images[index])
users.append(user)
}
return users
}
func updateSearchResultsForSearchController(searchController:UISearchController) {
if let searchText = searchController.searchBar.text?.lowercaseString {
if searchText.characters.count == 0 {
filteredUsers = allUsers
}
else {
filteredUsers = allUsers.filter {
return $0.name.lowercaseString.containsString(searchText) ||
$0.streetName.lowercaseString.containsString(searchText)
} ?? []
}
}
self.resultsController.tableView.reloadData()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredUsers ? filteredUsers.count : 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = self.tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
let user = filteredUsers[indexPath.row]
cell.photo.image = user.image
cell.name.text = user.name
cell.streetName.text = user.streetName
return cell
}

Related

table view does not reload after delete letter in search bar

I am trying to search contact in my app. I am using search bar to do that.
Lets suppose that I have a 2 contacts, Tolga and Toygun. When I type for "To" in searchbar both contact appears in table view. Then I type for "Toy" in searchbar no one appears in table view as should be. The problem is when I delete the letter y in "Toy" no one continues to appear. I want to see both contact in table view when I delete letter y but I couldn't.
Here is my code:
class ContactsVC: UIViewController {
//MARK: - Proporties
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var emptyView: UIView!
let fireStoreDatabase = Firestore.firestore()
var contactArray = [Contact]()
var tempContactArray = [Contact]()
var letters: [Character] = []
var tempLetters: [Character] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
searchBar.delegate = self
hideKeyboardWhenTappedAround()
getDataFromFirebase()
}
//MARK: - Function to Get Contacts Data From Firebase
func getDataFromFirebase(){
fireStoreDatabase.collection("Contacts").order(by: "contactName").addSnapshotListener { (snapshot, err) in
if err == nil {
if snapshot?.isEmpty == false && snapshot != nil {
self.contactArray.removeAll(keepingCapacity: false)
for document in snapshot!.documents {
if let uid = document.get("uid") as? String {
if uid == self.userId {
if let contactUrl = document.get("contactUrl") as? String,
let contactName = document.get("contactName") as? String,
let contactSirname = document.get("contactSirname") as? String,
let contactPhone = document.get("contactPhone") as? String,
let contactEmail = document.get("contactEmail") as? String,
let contactBloodgroup = document.get("contactBloodGroup") as? String,
let contactBirthday = document.get("contactBirthday") as? String{
self.contactArray.append(Contact(contactUrl: contactUrl, contactName: contactName, contactSirname: contactSirname, contactPhone: contactPhone, contactEmail: contactEmail, contactBloodgroup: contactBloodgroup, contactBirthday: contactBirthday, documentId: document.documentID))
}
}
}
}
self.tempContactArray = self.contactArray
//Section
self.letters.removeAll(keepingCapacity: false)
self.letters = self.contactArray.map({ (contact) in
return contact.contactName.uppercased().first!
})
self.letters = self.letters.sorted()
self.letters = self.letters.reduce([], { (list, name) -> [Character] in
if !list.contains(name) {
return list + [name]
}
return list
})
self.tempLetters = self.letters
self.tableView.reloadData()
} else {
self.contactArray.removeAll(keepingCapacity: false)
self.tableView.reloadData()
}
if(self.contactArray.count == 0) {
self.emptyView.isHidden = false
self.tableView.isHidden = true
}else{
self.emptyView.isHidden = true
self.tableView.isHidden = false
}
}
}
}
//MARK: - Section after search
func getLetters(contact: [Contact]) {
letters.removeAll(keepingCapacity: false)
letters = contact.map({ (contact) in
return contact.contactName.uppercased().first!
})
letters = letters.sorted()
letters = letters.reduce([], { (list, name) -> [Character] in
if !list.contains(name) {
return list + [name]
}
return list
})
}
//MARK: - Table View Data Source
extension ContactsVC: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
letters.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return letters[section].description
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contactArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ContactsViewCell
if letters[indexPath.section] == contactArray[indexPath.row].contactName.uppercased().first {
cell.contactImage.sd_setImage(with: URL(string: contactArray[indexPath.row].contactUrl))
cell.contactFullNameLabel.text = contactArray[indexPath.row].contactName + " " + contactArray[indexPath.row].contactSirname
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if letters[indexPath.section] == contactArray[indexPath.row].contactName.uppercased().first {
return 100.0
} else {
return 0.0
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(identifier: "AddContactVC") as! AddContactVC
vc.isNewContact = false
vc.documentId = contactArray[indexPath.row].documentId
vc.contact = contactArray[indexPath.row]
self.present(vc, animated: true, completion: nil)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
view.endEditing(true)
}
}
//MARK: - Search Bar
extension ContactsVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print(searchText)
letters.removeAll(keepingCapacity: false)
if searchText.isEmpty == false {
contactArray = contactArray.filter{$0.contactName.lowercased().contains(searchText.lowercased())}
getLetters(contact: contactArray)
} else {
contactArray = tempContactArray
letters = tempLetters
}
self.tableView.reloadData()
}
}
This line causes the problem.
contactArray = contactArray.filter{$0.contactName.lowercased().contains(searchText.lowercased())}
let's consider the same example you mentioned. You have two contacts 'Tolgo' and 'Toygun'. When you type 'To', You filter the contacts and again assign it to the contactArray. So now contactArray will have two contacts Tolgo and Toygun. When you type 'Toy', again you apply filter on those 2 contacts in contactArray and assign to contactArray again. Now you will have only one contact detail 'Toygun' in contactArray. You are deleting 'y' from 'toy' search keyword, now you apply filter on contactArray which only has one contact(toygun). This causes only one contact to show in table
Solution:
Have all your fetched contacts in contactArray. On searching, filter from this array and assign the filtered items to tempContactArray. Have tempContactArray as the source array.
I hope i am able to help you solve your problem.
You can also implement UISearchController UISearchResultsUpdating protocol with function updateSearchResults and handle all your changes there. Here is a smooth tutorial: https://www.raywenderlich.com/4363809-uisearchcontroller-tutorial-getting-started

SearchController filtering problem in TableView

I've next model:
struct HashTags {
var title = ""
var tags = [String]()
}
and I try use UISearchController for seach text in tags array.
I impelemented UISearchController in my TableViewController:
class TagsController: UITableViewController {
var hashtags = [HashTags]()
var filtered = [HashTags]()
let searchController = UISearchController(searchResultsController: nil)
var searchBarIsEmpty: Bool {
guard let text = searchController.searchBar.text else { return false }
return text.isEmpty
}
var isFiltering: Bool {
return searchController.isActive && !searchBarIsEmpty
}
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search"
navigationItem.searchController = searchController
definesPresentationContext = true
}
override func numberOfSections(in tableView: UITableView) -> Int {
if isFiltering {
return filtered.count
}
return hashtags.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if isFiltering {
return filtered[section].title
}
return hashtags[section].title
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering {
return filtered[section].tags.count
}
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Tags", for: indexPath)
var item = HashTags()
if isFiltering {
item = filtered[indexPath.section]
} else {
item = hashtags[indexPath.section]
}
let tags = item.tags
cell.textLabel?.text = tags.joined(separator: ", ")
return cell
}
}
extension TagsController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
func filterContentForSearchText(_ searchText: String) {
filtered = hashtags.filter({ (hashtag: HashTags) -> Bool in
return hashtag.tags.contains(searchText.lowercased())
})
tableView.reloadData()
}
}
But it doesn't work as it should and when I start type text I receive blank screen. I need UISearchController work correctctly and search for text in HashTags tags arrays, showing correct number of sections and rows.

Filter result(s) in array of class (Swift 3)

In Class
class Objects {
var number: Int!
var name: String!
init(number: Int, name: String) {
self.number = number
self.name = name
}
}
In viewController
var allObjects = [Objects]()
var inSearchMode = false
#IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
if inSearchMode {
let fill: Objects!
fill = filteredObject[indexPath.row]
cell.configureCell(fill)
} else {
let fill: Objects!
fill = allObjects[indexPath.row]
cell.configureCell(fill)
return cell
}
}
return UITableViewCell()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if inSearchMode {
return filteredObject.count
}
return allObjects.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text == nil || searchBar.text == "" {
inSearchMode = false
tableView.reloadData()
view.endEditing(true)
} else {
inSearchMode = true
var lowerCase = Int(searchBar.text!)
filteredObject = allObjects.filter({$0.number == lowerCase})
tableView.reloadData()
print(filteredObject)
}
}
I would like to have a search bar that filters and shows only one result that contains the number we are looking for. I thought about using contains and put the number we input from the search bar.
I manage to get one object into the filteredObject, but it won't show up in the tableView
Look carefully at this part of your code:
if inSearchMode {
let fill: Objects!
fill = filteredObject[indexPath.row]
cell.configureCell(fill)
} else {
let fill: Objects!
fill = allObjects[indexPath.row]
cell.configureCell(fill)
return cell
}
You did not return a cell when you are in search mode, that is why you do not see anything when searching.
I would use a computed property to drive the tableview; This property is either all the objects or the filtered object(s):
var allObjects = [Objects]()
var filteredObjects: [Objects]?
var objects: [Objects] = {
return filteredObjects ?? allObjects
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
let fill = self.objects[indexPath.row]
cell.configureCell(fill)
return cell
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text == nil || searchBar.text == "" {
self.filteredObjects = nil
} else {
var lowerCase = Int(searchBar.text!)
self.filteredObjects = allObjects.filter({$0.number == lowerCase})
}
tableView.reloadData()
}

How to return multiple values in swift

I'm starter in swift.
I create tableview and get data from jsonFile to show text and picture.
Then I want to add searchBar on tableview but have problem.
import UIKit
class EpisodesTableViewController: UITableViewController
{
var episodes = [Episode]()
var names = [Episode]()
let searchController = UISearchController(searchResultsController: nil)
var filteredNames = [Episode]()
func filterContentForSearchText(searchText: String) {
filteredNames = self.names.filter { name in
return name.title!.lowercaseString.containsString(searchText.lowercaseString)
}
tableView.reloadData()
}
override func viewDidLoad()
{
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
tableView.setContentOffset(CGPoint(x: 0, y: searchController.searchBar.frame.size.height), animated: false)
tableView.estimatedRowHeight = tableView.rowHeight
tableView.rowHeight = UITableViewAutomaticDimension
tableView.separatorStyle = .None
self.episodes = Episode.downloadAllEpisodes()
self.tableView.reloadData()
}
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int
{
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if searchController.active && searchController.searchBar.text != ""{
return filteredNames.count
}else{
return names.count
}
return episodes.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("Episode Cell", forIndexPath: indexPath) as! EpisodeTableViewCell
let episode = self.episodes[indexPath.row]
let data: Episode
if searchController.active && searchController.searchBar.text != "" {
data = filteredNames[indexPath.row]
}
else {
data = names[indexPath.row]
}
let titleName = data.title!
cell.episode = episode
cell.textLabel?.text = titleName
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SendData"{
if let detailPage = segue.destinationViewController as? detailEpisodeViewController {
if let indexpath = tableView.indexPathForSelectedRow {
let episode = episodes[indexpath.row]
detailPage.episode = episode
}
}
}
}
}
extension EpisodesTableViewController: UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
this my code.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if searchController.active && searchController.searchBar.text != ""{
return filteredNames.count
}else{
return names.count
}
return episodes.count
}
When I return filteredNames and names interface just show seachbar. If I return filtered names and episodes show error index out of range.
I don't know How to fix that.
If you want to return two values just return a touple like so:
return (DataType, DataType)
so this could be
func returnTouple() -> (String, AnyObject) {
return ("Hello World", 1)
}
then you would access it like so:
let (myString, myObject) = returnTouple()
and myString == "Hello World"
You could also access both throught .0 and .1 like returnTouple().0 == "Hello World"
Next,
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searchController.active && searchController.searchBar.text != "" {
return filteredNames.count
} else {
return names.count
}
return episodes.count
}
This function shouldn't work. You have an if {} else {} with a return statement in both sections. Unless you said if {} else if {} this makes the thrid return statement impossible to hit so it shouldn't be there.

Swift - Array index out of range warning

My code has an issue when I run the iOS simulator. It breaks and brings me to the line of code:let targetUser = users[indexPath.row] and says 'EXC_BAD_INSTRUCTION (CODE=EXC_1386_INVOP,snbcode = 0x0)' would anyone be able to help me figure out why?
class OverviewTableViewController: UITableViewController {
#IBOutlet weak var LogoutButton: UIBarButtonItem!
#IBOutlet weak var ChoosePartnerButton: UIBarButtonItem!
var rooms = [PFObject]()
var users = [PFUser]()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.setLeftBarButtonItem(LogoutButton, animated: false)
self.navigationItem.setRightBarButtonItem(ChoosePartnerButton, animated: false)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if PFUser.currentUser() != nil {
loadData()
}
}
func loadData() {
rooms = [PFObject]()
users = [PFUser]()
self.tableView.reloadData()
let pred = NSPredicate(format: "user1 = %# OR user2 = %#", PFUser.currentUser()!, PFUser.currentUser()!)
let roomQuery = PFQuery(className: "Room", predicate: pred)
roomQuery.includeKey("user1")
roomQuery.includeKey("user2")
roomQuery.findObjectsInBackgroundWithBlock { (results:[AnyObject]?, error:NSError?) -> Void in
if error == nil {
self.rooms = results as! [PFObject]
for room in self.rooms {
let user1 = room.objectForKey("user1") as! PFUser
let user2 = room["user2"] as! PFUser
if user1.objectId != PFUser.currentUser()?.objectId {
self.users.append(user1)
}
if user2.objectId != PFUser.currentUser()?.objectId {
self.users.append(user2)
}
}
self.tableView.reloadData()
}
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return rooms.count
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 80
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! OverviewTableViewCell
let targetUser = users[indexPath.row]
cell.nameLabel.text = targetUser.username
return cell
}
Sounds like #MartinR had nailed it. You're reading rooms.count in numberOfRowsInSection, but then looking up data from the users array in cellForRowAtIndexPath.
You could figure this out in the debugger by examining indexPath.row when you crash, and examining the size of rooms.count as well.

Resources