I have a struct like this:
struct StructName {
var name: String?
var contents: [Content]?
}
Content looks like this:
struct Content {
var contentName: String?
}
Right now I am just searching by the names inside my array of StructName
var array = [StructName]()
func searchBar(_ searchBar: UISearchBar, textDidChange searchText:String) {
var filteredArray = array.filter { $0.name?.range(of: searchText, options: [.caseInsensitive]) != nil}
}
How would I approach this if I want to search inside the elements in contents simultaneous with name in StructName?
If I structured it like this:
let contentA = [Content(contentName: "Content_1"), Content(contentName: "Content_2")]
let contentB = [Content(contentName: "Content_1"), Content(contentName: "Content_2")]
array[0] = (StructName(name: "Name_1", contents: contentA))
array[1] = (StructName(name: "Name_2", contents: contentB))
And I wrote "NC" in my searchText, it should return everything, since there is an "N" in "Name_1" and "Name_2" and there is a "C" in "Content_1" and "Content_2".
Example of filteredArray:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredArray = array.filter{ elements in
var result = false
elements.contents.forEach{ content in
if (content.contentName.lowercased().contains(searchText.lowercased()) ) {
result = true
}
}
if (content.name?.lowercased().contains(searchText.lowercased()))!{
result = true
}
return result
}
}
Note that this does not search simultaneously as I have described.
If you want to filter and find StructNames where Content array items compares with searchBar text, when look at this code
struct Content {
var contentValue: String
}
struct StructName{
var name: String?
var contents: [Content]?
}
var array = [ StructName(name: "test1", contents: [ Content(contentValue: "contentValue1"), Content(contentValue: "contentValue2"), Content(contentValue: "searchedText") ]) ]
let searchedText = "searchedText"
let filteredArray = array.filter{ structName in
var result = false
structName.contents?.forEach{ content in
if (content.contentValue.lowercased().contains(searchedText.lowercased()) ) {
result = true
}
}
return result
}
Your usage of the word simultaneous is not clear enough to describe what sort of search result you expect.
I assume,
if (and only if) name matches searchText or any contentName in contents matches searchText, the StructName instance appears in the filteredArray.
(matches is defined similar as your name in your post.)
Then you can write something like this:
var filteredArray = array.filter {
$0.name?.range(of: searchText, options: .caseInsensitive) != nil
|| $0.contents?.contains {content in content.contentName?.range(of: searchText, options: .caseInsensitive) != nil} == true
}
If this is not what you want, you may need to specify your search condition more precisely.
(By the way, name or contents needs to be Optional?)
(For your updated example)
Seems you want character based searching with every character needs to hit.
Using a little bit modified example:
var array = [StructName]()
let contentA = [Content(contentName: "Content_1"), Content(contentName: "Content_2")]
let contentB = [Content(contentName: "Content_3"), Content(contentName: "Content_4")]
array.append(StructName(name: "Name_1", contents: contentA))
array.append(StructName(name: "Name_2", contents: contentB))
with some debugging help:
extension StructName: CustomStringConvertible {
var description: String {
return "\(name ?? ""):\(contents?.map{$0.contentName ?? ""}.joined(separator: ";") ?? "")"
}
}
print(array) //->[Name_1:Content_1;Content_2, Name_2:Content_3;Content_4]
You can write something like this:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText:String) {
let filteredArray = array.filter {element in
!searchText.lowercased().contains {searchChar in
!(element.name ?? "").lowercased().contains(searchChar)
&& !(element.contents ?? []).contains {content in (content.contentName ?? "").lowercased().contains(searchChar)}
}
}
print(filteredArray)
}
Output example:
searchBar(UISearchBar(), textDidChange: "NC") //->[Name_1:Content_1;Content_2, Name_2:Content_3;Content_4]
searchBar(UISearchBar(), textDidChange: "1") //->[Name_1:Content_1;Content_2]
searchBar(UISearchBar(), textDidChange: "2") //->[Name_1:Content_1;Content_2, Name_2:Content_3;Content_4]
searchBar(UISearchBar(), textDidChange: "12") //->[Name_1:Content_1;Content_2]
Though, I'm not sure if this is what you expect, as your description is not clear enough yet.
Related
In Swift 5.3, in my searchbar, i want to display a result tableview where each of its cells must contain all of the searched words, regardless of the order in which they were written in the searchbar.
Example:
i have a struct of objects:
struct Object {
var story : String
var age: Int
...
}
my tableview display this array before the search:
Array : [Object] = [
Object(story:"English texts for beginners to practice", age: 1200),
Object(story:"reading and comprehension online", age: 1600),
Object(story:"and for free. Practicing your comprehension", age: 1800),
Object(story:"of written English will both improve", age: 1100),
Object(story:"your vocabulary and comprehension", age: 1500),
Object(story:"of grammar and word order." age: 1400)
]
And when i tap "comprehension your" in the searchbar, it will retrieves this filtered array:
filteredArray = [
Object(story:"and for free. Practicing your comprehension", age: 1800),
Object(story:"your vocabulary and comprehension", age: 1500)
]
And of course i have this function of UISearchBar
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
filteredArray = Array // To see all the Array
self.tableView.reloadData()
}
Here is what i have tried, but it's not what i need
if let searchBarText = searchBar.text{
let searchText = searchBarText.lowercased()
filteredArray = Array.filter ({$0.story.lowercased().range(of: searchText.lowercased()) != nil})
filteredArray += Array.filter ({$0.story.lowercased().range(of: searchText.lowercased()) != nil})
tableView.reloadData()
}
You can create a Set of the given search words and then use filter where all the words in the search text are checked against the story using allSatisfy
let words = Set(searchText.split(separator: " ").map(String.init))
let filtered = array.filter { object in words.allSatisfy { word in object.story.localizedCaseInsensitiveContains(word) } })
I have implemented code for searchBar and it works if I either search the first or the last name of a person. For example, if I want to search Kate Bell, the search works if I write "Kate", and it works if I write "Bell". But if I write "Kate B" the search result disappear.
Here's my code:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.count == 0 {
isFiltered = false
tableViewOutlet.reloadData()
}else {
isFiltered = true
searchInternalArray = contactsInternalArray.filter({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
searchArrayGroups = sectionsArrayGroups.filter({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
searchArraySAEs = sectionsArraySAEs.filter ({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
tableView.reloadData()
}
I fond this on SO How to search by both first name and last name
It's in objective-c and I have trouble implementing it into my own code. I tried something like this:
searchInternalArray = contactsInternalArray.filter({ object -> Bool in
guard let text = searchBar.text?.range(of: object.firstName + object.lastName) else {return false}
return object.firstName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)) || object.lastName.lowercased().contains(text.lowercased().trimmingCharacters(in: .whitespacesAndNewlines))
})
That's obviously not how it should be implemented though.
Edit
Didn't think it was relevant, but perhaps it is: I'm filtering multiple arrays since I have data coming from three different sources to the tableView. The whole searchBar code looks like above. It's updated.
Unless I'm missing something, there's a far simpler solution to this:
let name = firstname.lowercased() + " " + lastname.lowercased()
return name.contains(text.lowercased())
This works for any part of forename, surname, or the a part of the two with a space between. You should probably still trim leading/trailing spaces, and if you wanted to get rid of issues with variable spaces between the first and last names you could search/replace them double spaces with a single space.
Why not just create the full name from the two parts and search based on that.
let searchResults = contactsInternalArray.filter { object in
"\(object.firstName) \(object.lastName)".contains(searchText)
}
You can still lowercase them and trim if you want to.
I got idea from flanker's answer.
This works for me
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.searchBar.showsCancelButton = true
if searchText != ""{
self.isSearchActive = true
if self.object.count != 0{
filteredContacts = self. object.filter(){
//("\(($0 as FetchedContact).firstName.lowercased()) \(($0 as FetchedContact).object.lowercased()) \(($0 as FetchedContact).telephone.lowercased())")
let name = ("\(($0 as ContactModel).firstName.lowercased()) \(($0 as ContactModel).lastName.lowercased()) \(($0 as ContactModel).mobileNumber.lowercased())")
return name.contains(searchText.lowercased())
}
}
self.tblContactList.reloadData()
}
}
Which search on the base of FirsName, LastName and MobileNumber for Contacts.
I have a UISearchBar and I'm filtering a UICollectionView by search term like this:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredRecipes = getRecipes().filter({( recipe : Recipe ) -> Bool in
return recipe.name.lowercased().contains(searchText.lowercased())
})
if searchText == "" || searchText == " " {
filteredRecipes = getRecipes()
}
}
Each Recipe has a few properties and one of them by which I want to filter is name, that is made up of a couple of words like "Red Velvet Cake". Right now, the function would return this recipe if I searched for anything from:
red
r
lvet
ed velvet
How do I make it so that it filters the recipes based on the start of each of the words in the name. So it would only return the recipe if I searched for:
red
velvet
cake
velvet cake
And also so that it returns all recipes if the search terms is nothing, or just empty spaces?
I have looked here for answers and I can only find solutions similar to my existing one. Thank you!
Use combination of filter(_:), contains(where:) and hasPrefix(_:) like so,
filteredRecipes = getRecipes().filter({
let components = $0.name.components(separatedBy: " ")
return components.contains(where: {
$0.lowercased().hasPrefix(searchText.lowercased())
})
})
You can use enumerateSubstrings method and compare each word of recipe.name with searchText
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard !searchText.trimmingCharacters(in: .whitespaces).isEmpty else {
filteredRecipes = getRecipes()
return
}
filteredRecipes = getRecipes().filter({
var result = false
$0.name.enumerateSubstrings(in: $0.name.startIndex..<$0.name.endIndex, options: [.byWords], { (word, _, _, _) in
if let word = word, word.caseInsensitiveCompare(searchText) == .orderedSame {
result = true
}
})
return result
})
}
To search for a string included in a struct I use:
let results = myArray.filter( {$0.model.localizedCaseInsensitiveContains("bu")} )
But say the struct has several properties that I'd like to search - or maybe I'd even like to search all of them at one time. I can only filter primitive types so leaving 'model' out won't work.
Solution -------------------------
While I really liked the idea of using key paths as Matt suggested below, I ended up adding a function to my struct that made my view controller code much cleaner:
struct QuoteItem {
var itemIdentifier: UUID
var quoteNumber: String
var customerName: String
var address1: String
func quoteItemContains(_ searchString: String) -> Bool {
if self.address1.localizedCaseInsensitiveContains(searchString) ||
self.customerName.localizedCaseInsensitiveContains(searchString) ||
self.quoteNumber.localizedCaseInsensitiveContains(searchString)
{
return true
}
return false
}
Then, in my controller, quotes is an array of QuoteItem that I can search by simply writing:
searchQuoteArray = quotes.filter({ $0.quoteItemContains(searchString) })
This sounds like a job for Swift key paths. Just supply the key paths for the String properties you want to search.
struct MyStruct {
let manny = "Hi"
let moe = "Hey"
let jack = "Howdy"
}
let paths = [\MyStruct.manny, \MyStruct.moe, \MyStruct.jack]
let s = MyStruct()
let target = "y"
let results = paths.map { s[keyPath:$0].localizedCaseInsensitiveContains(target) }
// [false, true, true]
I hope i understood you correct. I think with this piece of code you can achieve what you want:
struct ExampleStruct {
let firstSearchString: String
let secondSearchString: String
}
let exampleOne = ExampleStruct(firstSearchString: "Hello", secondSearchString: "Dude")
let exampleTwo = ExampleStruct(firstSearchString: "Bye", secondSearchString: "Boy")
let exampleArray = [exampleOne, exampleTwo]
let searchString = "Hello"
let filteredArray = exampleArray.filter { (example) -> Bool in
// check here the properties you want to check
if (example.firstSearchString.localizedCaseInsensitiveContains(searchString) || example.secondSearchString.localizedCaseInsensitiveContains(searchString)) {
return true
}
return false
}
for example in filteredArray {
print(example)
}
This prints the following in Playgrounds:
ExampleStruct(firstSearchString: "Hello", secondSearchString: "Dude")
Let me know if it helps.
I have a 2D array in Swift.
let Name:[[String]] =[["India", "PA"], ["Africa", "SA"]]
I am trying to implement a search bar in the iOS app and trying to filter this 2D Array.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String, indexPath: IndexPath) {
filteredArray = Name.filter ({(names: String) -> Bool in
return names.lowercased().range(of: searchText.lowercased()) != nil
})
if searchText = ""
{
shouldShowSearchResult = true
self.tableView.reloadData()
}
else
{
shouldShowSearchResult = false
self.tableView.reloadData()
}
But I am getting a compilation error:
cannot convert value of type (String) -> Bool to expected argument
type'([String]) -> Bool'
Minimal, Complete, and Verifiable example
First of all you should always post a Minimal, Complete, and Verifiable example.
So the question does become
Given an array like this
let data = [["India", "PA"], ["Africa", "SA"]]
And given a keyword like this
let keyword = "ind"
How can I retrieve only the elements (sub-arrays) containing the keyword?
Solution
The answer is
let lowercasedKeyword = keyword.lowercased()
let filtered = data.filter {
$0[0].lowercased().range(of: lowercasedKeyword) != nil
|| $0[1].lowercased().range(of: lowercasedKeyword) != nil }
Considerations
Using and array of arrays as model is a very dangerous design choice.
Why don't you create a model like this?
struct Country {
let name: String
let code: String
}
And then build your array as shown below?
let countries = [Country(name: "India", code: "PA"), Country(name: "Africa", code: "SA")]