I have just started to learn swift and i am looking at the tableview and searchbar feature. Below i have my array which is a list of fruits:
var fruits: [[String]] = [["Apple", "Green"],["Pear", "Green"], ["Banana", "Yellow"], ["Orange", "Orange"]]
I have them in a table view with the name of the fruit as the title and the colour as a subtitle. I am trying to use the search bar to filter but i cant seem to get it right. I only want to search for the name of the fruit not the colour.
var filteredFruits = [String]()
var shouldShowSearchResults = false
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredFruits.removeAll()
var i = 0
while i < fruits.count
{
var filteredFruits = fruits[i].filter ({ (fruit: String) -> Bool in
return fruit.lowercased().range(of: searchText.lowercased()) != nil
})
if searchText != ""
{
shouldShowSearchResults = true
if filteredItems.count > 0
{
filteredFruits.append(filteredItems[0])
filteredItems.removeAll()
}
}
else
{
shouldShowSearchResults = false
}
i += 1
}
self.tableView.reloadData()
}
I do get results returned but it mixes up the subtitles and the titles as well as not returning the correct results. Can anyone point me in the right direction?
I do not understand why you iterate over the fruits using some kind of while loop. Instead I would propose you take advantage of a function like:
func filterFruits(searchText: String) -> [[String]] {
guard searchText != "" else {
return fruits
}
let needle = searchText.lowercased()
return fruits.filter {fruitObj in
return fruitObj.first!.lowercased().contains(needle)
}
}
That function returns all fruits that have a name containing the searchText.
filterFruits(searchText: "g") yields [["Orange", "Orange"]]
If you want to search through all attributes use something like:
func filterFruits(searchText: String) -> [[String]] {
guard searchText != "" else {
return fruits
}
let needle = searchText.lowercased()
return fruits.filter {fruitObj in
return fruitObj.contains { attribute in
attribute.lowercased().contains(needle)
}
}
}
filterFruits(searchText: "g") yields [["Apple", "Green"], ["Pear", "Green"], ["Orange", "Orange"]]
To get you on the right track for the future: you should really introduce a Fruit class which holds all relevant information of one specific fruit instance. Then you can use the first function and do something like fruitObj.matches(searchText) where you define a func inside the Fruit class which determines if the fruit matches the search.
How about this?
var filteredFruits = fruits.filter ({ (fruitData: [String]) -> Bool in
let fruit = fruitData[0]
return fruit.lowercased().range(of: searchText.lowercased()) != nil
})
I see your code is too complicated, let keep everything is simple.
let fruits: [[String]] = [["Apple", "Green"],["Pear", "Green"], ["Banana", "Yellow"], ["Orange", "Orange"]]
var filteredFruits = [String]()
var shouldShowSearchResults = false
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredFruits.removeAll()
if searchText.isEmpty {
// do something if searchText is empty
} else {
let arr = fruits.filter({ (fruit) -> Bool in
return fruit[0].lowercased().contains(searchText.lowercased())
})
filteredFruits = arr.map({ (fruit) -> String in // convert [[String]] -> [String]
return fruit[0]
})
}
self.tableView.reloadData()
}
I think it'll be better if the type of filteredFruits as same as fruits's type. And instead of [[String]], you can declare an array of type or tuple like this let fruits: [(name: String, color: String)] = [(name: "Apple", color: "Green")]
Related
I have a search bar and a table view and I want to put names of songs , artists and albums in them. Whenever I type in the search bar it only filters threw album names and not songs, or artists. How would I get them all to show up because only getAlbumName() is showing up? I just added Search.swift.
var search = [Search]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
if (searchActive) {
cell.textLabel?.text = search[indexPath.row].getCleanName()
cell.textLabel?.text = search[indexPath.row].getArtistId()
cell.textLabel?.text = search[indexPath.row].getAlbumName()
} else {
searchActive = false
}
return cell;
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
search = search.filter({ (songName) -> Bool in
return songName.songname.lowercased().range(of: searchText.lowercased()) != nil
})
if(search.count == 0) {
searchActive = false;
} else {
searchActive = true;
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Search.swift
class Search {
var search: Search!
var id = Int()
var name = String()
var cleanName = String()
var artist = String()
var album = String()
init?(id:String, name:String, artist:String, album:String) {
self.id = Int(id)!
self.name = name
self.cleanName = name.replacingOccurrences(of: "_", with: " ").replacingOccurrences(of: ".wav", with: "")
self.artist = artist
self.album = album
}
func getId() -> Int {
return id
}
func getName() -> String {
return name
}
func getCleanName() -> String {
return cleanName
}
func getArtistId() -> String {
return artist
}
func getAlbumName() -> String {
return album
}
}
If you want to search for multiple properties you have to add range(of expressions for each property
search = search.filter({ song -> Bool in
return song.cleanName.range(of: searchText, options: .caseInsensitive) != nil ||
song.artist.range(of: searchText, options: .caseInsensitive) != nil ||
song.album.range(of: searchText, options: .caseInsensitive) != nil
})
Consider that search = search.filter makes actually no sense because you overwrite the array with the filtered result and if you tap the backspace key you'll get unexpected behavior.
And most of your code in the class is redundant. The methods to access properties are pointless. It can be reduced to
class Search {
let id : Int
let name, cleanName, artist, album : String
init(id: String, name: String, artist: String, album: String) {
self.id = Int(id)!
self.name = name
self.cleanName = name.replacingOccurrences(of: "_", with: " ").replacingOccurrences(of: ".wav", with: "")
self.artist = artist
self.album = album
}
}
You can get the album easily with song.album
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()
}
I have added a search bar to my tableviewCell which has JSON data I have added a function which will be triggered when there is change of the text in the searchBar but I am getting an error.
This is my Item class:-
struct Item : Codable {
var name = String()
var symbol = String()
var checked : Bool = false
init(bitJSON: JSON) {
self.name = bitJSON["name"].stringValue
self.symbol = bitJSON["symbol"].stringValue
}
}
This is my tableview class:-
var items = [Item]()
let searchBar = UISearchBar()
var filteredArray = [String]()
var shouldShowSearchResults = false
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredArray = items.filter({ (names) -> Bool in // ERROR
return names.name.lowercased().range(of: searchText.lowercased()) != nil
})
if searchText != "" {
shouldShowSearchResults = true
self.tableView.reloadData()
}else {
shouldShowSearchResults = false
self.tableView.reloadData()
}
}
Filter simply generates a new array with certain elements from the oroginal array removed. If you wanted to generate a new array from that filtered array, you'd then do a map. Or, if you'd like to do it all in one step, you can do a flatMap:
filteredArray = items.flatMap { (names) -> String? in
if names.name.lowercased().range(of:searchText.lowercased()) != nil {
return names.name
}
else {
return nil
}
}
This will give you an array of all strings that meet your requirements.
Notes: In Swift 4.1, flatMap has been renamed to compactMap.
filter does not actually change the type of array. [Item] after filtering is still a [Item]. Therefore, you cannot assign it to a property of type [String].
To transform each element of an array to something else (String in this case), you should call map:
filteredArray = items.filter({ (names) -> Bool in
return names.name.lowercased().range(of: searchText.lowercased()) != nil
})
.map { $0.name } // this line!
Is there a way to search through a UITableView and ignore certain characters like commas or dots?
I.e. I would like to search for "St George" but my data set contains "St. George" so the result is always zero.
EDITED Q:
func filteredArray(searchText: NSString) {
if searchText == "" {
array_search = array_all
} else {
showTableViewResults()
array_search.removeAll()
for i in 0 ..< array_all.count {
let object:MyObject = array_all[i]
let languageSearchString = object.myObjectName
let searchStr:String = languageSearchString!
if searchStr.lowercased().contains(searchText.lowercased) {
array_search.append(object)
}
}
}
tableView.reloadData()
recordsFoundLabel.text = "records found: \(array_search.count)"
}
You can filter all characters thats not a letter out of your String before performing your search. The same applies to the table view data source elements. Also as mentioned by rmaddy you should implement a case insensitive search:
edit/update Swift 5.2 or later
extension StringProtocol {
func caseInsensitiveContains<S: StringProtocol>(_ string: S) -> Bool { range(of: string, options: .caseInsensitive) != nil }
}
extension StringProtocol where Self: RangeReplaceableCollection {
var letters: Self { filter(\.isLetter) }
}
Testing:
let search = "st george"
let tableViewSource = ["Apple", "Orange", "Banana", "St. George"]
let filtered = tableViewSource.filter {
$0.letters.caseInsensitiveContains(search.letters)
}
print(filtered) // ["St. George"]
If you would like to literally just remove punctuation from your String (note that would keep the spaces in your String), you can do as follow:
extension StringProtocol where Self: RangeReplaceableCollection {
mutating func removePunctuation() { removeAll(where: \.isPunctuation) }
}
extension Bool {
var negated: Bool { !self }
}
extension StringProtocol where Self: RangeReplaceableCollection {
var removingPunctuation: Self { filter(\.isPunctuation.negated) }
}
Testing:
let filtered = tableViewSource.filter {
$0.removingPunctuation.caseInsensitiveContains(search.removingPunctuation)
}
print(filtered) // ["St. George"]
If you would like to implement the same logic as Xcode autocomplete you would need to do a search for each character and change the startIndex of the string searched:
extension StringProtocol where Self: RangeReplaceableCollection {
func containsCharactersInSequence<S: StringProtocol>(_ string: S, options: String.CompareOptions = []) -> (result: Bool, ranges: [Range<Index>]) {
var found = 0
var startIndex = self.startIndex
var index = string.startIndex
var ranges: [Range<Index>] = []
while index < string.endIndex,
let range = self[startIndex...].range(of: string[index...index], options: options) {
ranges.append(range)
startIndex = range.upperBound
string.formIndex(after: &index)
found += 1
}
return (found == string.count, ranges)
}
}
Playground Testing:
let search = "stgre"
let tableViewSource = ["Apple", "Orange", "Banana", "St. George"]
let filtered = tableViewSource.filter {
$0.containsCharactersInSequence(search, options: .caseInsensitive).result
}
print(filtered) // ["St. George"]
I think you should simply implement a function that for any given string, will return te same string without any point, (or whatever you want to erase); such as :
func erase(characters: [String], fromText text: String) -> String {
var result = String()
for character in text.characters {
if !characters.contains(character) {
result += String(character)
}
}
return result
}
(I cant test it from where i am but you get the idea right ?)
Hope it helps
I have tableview with alphabetic sections from my database and I want to add search bar but i can't figure it out how to filter the data and implement it in the tableview.
My database store inside two structs:
one struct holding all the data.
second struct gets the first letter for the sections and the first struct as array.
My structs:
struct SentenceInfo { // First struct (holds all the data)
let name: String
let detail: String
let sentence: String
init(name: String, detail: String, sentence: String) {
self.name = name
self.detail = detail
self.sentence = sentence
}
}
struct SentenceNameSection { // Second struct (first letter and array of the first struct)
var firstLetter: String
var crimes: [SentenceInfo]
init(title: String, objects: [SentenceInfo]) {
firstLetter = title
crimes = objects
}
}
My tableView:
var sections : [SentenceNameSection]!
var crimeData = [SentenceNameSection]()
var filteredData = [SentenceNameSection]()
var shouldShowSearchResults = false
var searchController: UISearchController!
func updateSearchResults(for searchController: UISearchController) {
let searchString = searchController.searchBar.text
filteredData = crimeData.filter({ (crime) -> Bool in
let crimeMatch: String = crime // Error about types
return ((crimeMatch.range(of: searchString!) != nil))
})
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: sentenceTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifer, for: indexPath) as! sentenceTableViewCell
let crime: SentenceInfo = sections[indexPath.section].crimes[indexPath.row]
cell.nameLabel.text = crime.name
cell.detailLabel.text = crime.detail
cell.sentenceLabel.text = crime.sentence
return cell
}
First of all crimeData contains SentenceNameSection which cannot be compared to String
Apart from that to filter the data source array including sections you have to use a repeat loop and create new SentenceNameSection items
This code searches for all three properties in the SentenceInfo struct
let searchString = searchController.searchBar.text!
filteredData.removeAll() // is mandatory to empty the filtered array
for section in crimeData {
let filteredContent = section.crimes.filter { $0.name.range(of: searchString) != nil
|| $0.detail.range(of: searchString) != nil
|| $0.sentence.range(of: searchString) != nil
}
if !filteredContent.isEmpty {
filteredData.append(SentenceNameSection(title: section.firstLetter, objects: filteredContent))
}
}
Note: Of course you have to handle the search case in all appropriate table view data source and delegate methods.
For Swift 3 , below is the sample code
Struct BookDetails{
var title:String?
var author:String?
}
var filteredSearch:[BookDetails] = []
filteredSearch = self.bookDetails.filter { (data) -> Bool in
return data.title?.range(of: searchText, options: String.CompareOptions.caseInsensitive) != nil || data.author?.range(of: searchText, options: String.CompareOptions.caseInsensitive) != nil
}