Ive been following this example https://www.raywenderlich.com/113772/uisearchcontroller-tutorial
Ive incorporated sqlite to fill the tableview, but currently the search is using substrings with .contains.
func filterContentForSearchText(searchText: String, scope: String = "All") {
filteredFood = food.filter { candy in
return candy.name.lowercaseString.containsString(searchText.lowercaseString)
}
tableView.reloadData()
}
Ive looked up a few different ways, NSPredicates and Regex, but Im not quite sure how to incorporate them correctly, or if thats what I even need to do.
Ex.Cell is "Stackoverflow is so amazing!"
If i search for Stackoverflow, the search is fine, but if I search "so is" I get no results.
You are looking for a more customized search method, which you would have to develop yourself.
For the example you provided, this code searches for each individual word to match:
let searchTerms = searchText.componentsSeparatedByString(" ").filter { $0 != "" }
filteredFood = food.filter { candy in
for term in searchTerms{
if !candy.name.lowercaseString.containsString(term.lowercaseString){
return false
}
}
return true
}
You can use regex for this using NSRegularExpression. For a regex to do a keyword search, I think the best way to do it is to do it in the following way: (str1|str2|str3).
So, in swift you can create replace the spaces with '|' and then use regular expressions:
let rtext = searchText.stringByReplacingOccurrencesOfString(" ", withString: "|");
let regex = NSRegularExpression(pattern: "(\(rtext))", .CaseInsensitive);
filteredFood = food.filter { candy in
return regex.numberOfMatchesInString(candy.name, options: 0, range: NSRange(0, candy.name.characters.count) > 0;
}
(Note haven't tested the code)
You can search in more than one label for example you have a class that contains state and city. So if the city does not exist it shows the rest of the cities in the state.
Here is what I am taking about:
// Set up the search text
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Create and divid the string into substring such as "You are" to "You" and "are"
let searchTerm = searchText.components(separatedBy: " ").filter{ $0 != "" }
// Whatever is being filtered is assigned to rilteredArrayPropertys
filteredArrayPropertys = arrayPropertys.filter({ (state) -> Bool in
// Search each term or substring
for term in searchTerm{
// Check which substring is equal state.propertyState, and if it statsifies it will return and assign to state.propertyState.
// .range(of: ): is what is being typed into search bar. state.propertyState is what has been setted.
if (state.propertyState.lowercased().range(of: term.lowercased()) != nil) || (state.propertyCity.lowercased().range(of: term.lowercased()) != nil) {
return true
}
}
return false
})
if searchText != "" {
shouldShowSearchResults = true
self.tableView.reloadData()
}
else {
shouldShowSearchResults = false
self.tableView.reloadData()
}
}
Related
I'm trying to filter an array of strings and return the strings that match based on two use cases.
Case 1:
Only match if the searchString is at the beginning of the word.
For eg, if we have an array ->
["Ralph Breaks The Internet", "Bohemian Rhapsody", "Spider-Man: Into the Spider-Verse"]
and we are trying to match it with a search string "r"
In this case, we should return ["Ralph Breaks The Internet", "Bohemian Rhapsody"] as "r" is at the beginning as in r in ralph and r in rhapsody. But "Spider-Man: Into the Spider-Verse" is not matched as the r is in the middle.
Case 2: Also match if the order of searchText is not exact.
For eg, if we have an array -> ["Ralph Breaks The Internet", "Bohemian Rhapsody", "Spider-Man: Into the Spider-Verse"] and we are trying to match it with a search string "Rhapsody Bohemian", it should still match even though the order is not the same.
Here's what I have tried so far:
func searchMovies(_ movieNames: [String], with searchText: String) -> [String] {
var matchedMovies = [String]()
for movie in movieNames {
let movieWords = movie.split(separator: " ")
let searchTextWords = searchText.split(separator: " ")
var count = searchTextWords.count
loop:
for word in movieWords {
for text in searchTextWords {
let pattern = "\\b\(text)"
if let _ = word.range(of: pattern, options: [.regularExpression, .caseInsensitive]) {
count -= 1
}
if count == 0 {
matchedMovies.append(movie)
break loop
}
}
}
}
return matchedMovies
}
I'm aware this is not an efficient way to do this. It would be great if someone can direct me in some direction so that I can solve the same thing more efficiently.
For your specific case, you can format your regex pattern like this:
"^(?=.*\\bRhapsody)(?=.*\\bBohemian).*$"
To make it flexible, you could write your func like this:
func searchMovies(_ movieNames: [String], with searchText: String) -> [String] {
// split search text into "words"
let words: [String] = searchText.components(separatedBy: " ")
// start of pattern string
var pattern: String = "^"
// for each word in search text
words.forEach { w in
// append regex to search for words beginning with word
pattern += "(?=.*\\b\(w))"
}
// end of pattern string
pattern += ".*$"
return movieNames.filter { (movie) -> Bool in
if let _ = movie.range(of: pattern, options: [.regularExpression, .caseInsensitive]) {
return true
}
return false
}
}
And you can call it with:
let a: [String] = [
"Ralph Breaks The Internet",
"Bohemian Rhapsody",
"Spider-Man: Into the Spider-Verse"
]
let matchingArray = searchMovies(a, with: "Rhapsody Bohemian")
Note that this will match the beginning of the word (as you showed using "r"), so this will return the same result:
let matchingArray = searchMovies(a, with: "Rhap Boh")
Declaration:
let listArray = ["kashif"]
let word = "kashif"
then this
contains(listArray, word)
Returns true but if declaration is:
let word = "Kashif"
then it returns false because comparison is case sensitive.
How to make this comparison case insensitive?
Xcode 8 • Swift 3 or later
let list = ["kashif"]
let word = "Kashif"
if list.contains(where: {$0.caseInsensitiveCompare(word) == .orderedSame}) {
print(true) // true
}
alternatively:
if list.contains(where: {$0.compare(word, options: .caseInsensitive) == .orderedSame}) {
print(true) // true
}
if you would like to know the position(s) of the element in the array (it might find more than one element that matches the predicate):
let indices = list.indices.filter { list[$0].caseInsensitiveCompare(word) == .orderedSame }
print(indices) // [0]
You can also use localizedStandardContains method which is case and diacritic insensitive and would match a substring as well:
func localizedStandardContains<T>(_ string: T) -> Bool where T : StringProtocol
Discussion This is the most appropriate method for doing user-level string searches, similar to how searches are done generally in the system. The search is locale-aware, case and diacritic insensitive. The exact list of search options applied may change over time.
let list = ["kashif"]
let word = "Káshif"
if list.contains(where: {$0.localizedStandardContains(word) }) {
print(true) // true
}
you can use
word.lowercaseString
to convert the string to all lowercase characters
For checking if a string exists in a array (case insensitively), please use
listArray.localizedCaseInsensitiveContainsString(word)
where listArray is the name of array
and word is your searched text
This code works in Swift 2.2
Swift 4
Just make everything (queries and results) case insensitive.
for item in listArray {
if item.lowercased().contains(word.lowercased()) {
searchResults.append(item)
}
}
You can add an extension:
Swift 5
extension Array where Element == String {
func containsIgnoringCase(_ element: Element) -> Bool {
contains { $0.caseInsensitiveCompare(element) == .orderedSame }
}
}
and use it like this:
["tEst"].containsIgnoringCase("TeSt") // true
Try this:
let loword = word.lowercaseString
let found = contains(listArray) { $0.lowercaseString == loword }
For checking if a string exists in a array with more Options(caseInsensitive, anchored/search is limited to start)
using Foundation range(of:options:)
let list = ["kashif"]
let word = "Kashif"
if list.contains(where: {$0.range(of: word, options: [.caseInsensitive, .anchored]) != nil}) {
print(true) // true
}
if let index = list.index(where: {$0.range(of: word, options: [.caseInsensitive, .anchored]) != nil}) {
print("Found at index \(index)") // true
}
swift 5, swift 4.2 , use the code in the below.
let list = ["kAshif"]
let word = "Kashif"
if list.contains(where: { $0.caseInsensitiveCompare(word) == .orderedSame }) {
print("contains is true")
}
SWIFT 3.0:
Finding a case insensitive string in a string array is cool and all, but if you don't have an index it can not be cool for certain situations.
Here is my solution:
let stringArray = ["FOO", "bar"]()
if let index = stringArray.index(where: {$0.caseInsensitiveCompare("foo") == .orderedSame}) {
print("STRING \(stringArray[index]) FOUND AT INDEX \(index)")
//prints "STRING FOO FOUND AT INDEX 0"
}
This is better than the other answers b/c you have index of the object in the array, so you can grab the object and do whatever you please :)
Expanding on #Govind Kumawat's answer
The simple comparison for a searchString in a word is:
word.range(of: searchString, options: .caseInsensitive) != nil
As functions:
func containsCaseInsensitive(searchString: String, in string: String) -> Bool {
return string.range(of: searchString, options: .caseInsensitive) != nil
}
func containsCaseInsensitive(searchString: String, in array: [String]) -> Bool {
return array.contains {$0.range(of: searchString, options: .caseInsensitive) != nil}
}
func caseInsensitiveMatches(searchString: String, in array: [String]) -> [String] {
return array.compactMap { string in
return string.range(of: searchString, options: .caseInsensitive) != nil
? string
: nil
}
}
My example
func updateSearchResultsForSearchController(searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
let countries = Countries.getAllCountries()
filteredCountries = countries.filter() {
return $0.name.containsString(searchText) || $0.name.lowercaseString.containsString(searchText)
}
self.tableView.reloadData()
}
If anyone is looking to search values from within model class, say
struct Country {
var name: String
}
One case do case insensitive checks like below -
let filteredList = countries.filter({ $0.name.range(of: "searchText", options: .caseInsensitive) != nil })
My code seems too long and it's not even complete!
I am trying to avoid a duplicate in a search bar.
I have it working for the first entry only.
Been playing around with this for a while but I'm sure it's more simple than what I seem to be making it.
var search : String = ("1, ")
let numbers = search.map { String($0) }
let duplicates = Array(Set(numbers.filter({ (i: String) in numbers.filter({ $0 == i }).count > 1})))
for item in duplicates
{
search = search.replacingOccurrences(of: item, with: "")
}
if mainSearchBar.text != search
{
mainSearchBar.text = (mainSearchBar.text ?? "") + search
}
The first number entered (with the comma and blank, is a total of 3 characters) doesn't get duplicated when entered on the second occasion which is great.
I want any number already in the search bar to not be duplicated at all.
The above code is the function of button 1.
There are a few buttons.
There is a UISearchBarDelegate method that can help you with that. I hope this helps you:
with this method you ensure no duplicated character will be in your new String
func removeDuplicates(in searchText: String) -> String {
var newString = ""
searchText.forEach { character in
if !newString.contains(character) {
newString.append(character)
}
}
return newString
}
And then you have to inherit from UISearchBarDelegate
extension **YourControllerName**: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchBar.text = removeDuplicates(in: searchText)
}
}
In order to get this working you must set mainSearchBar.delegate = self somewhere in your code. Usually in viewDidLoad.
Thanks to emelagumat, I managed to get it working like this.
func removeDuplicates(in searchText: String) -> String
{
let search : String = (mainSearchBar.text ?? "")
var newString = search
searchText.forEach
{
character in
if !newString.contains(searchText)
{
newString.append(searchText)
}
}
return newString
}
#objc func button1Tap()
{
let search : String = ("1, ")
mainSearchBar.text = removeDuplicates(in: search)
}
This question already has answers here:
How to check if a text field is empty or not in swift
(16 answers)
Checking if textfields are empty Swift
(6 answers)
Closed 4 years ago.
I'm currently working on a project where I use lots of UITextFields. For validation I need to check if the UITextFields are empty. I got a working solution, but it's not that elegant. Maybe someone knows a better way of doing it.
Here is my solution:
// Check if text field is empty
if let text = textField.text, !text.isEmpty {
// Text field is not empty
} else {
// Text field is empty
}
Is there a faster way without unwrapping the text attribute of the text field to find out if it's empty?
Thanks!
How about extending UITextField…
extension UITextField {
var isEmpty: Bool {
if let text = textField.text, !text.isEmpty {
return false
}
return true
}
}
so then…
if myTextField.isEmpty {
}
You can use UIKeyInput property hasText. It works for both UITextField and UITextView:
if textField.hasText {
// Text field is not empty
} else {
// Text field is empty
}
If you would like to check if the text has not only spaces on it:
extension UITextField {
var isEmpty: Bool {
return text?.trimmingCharacters(in: .whitespacesAndNewlines) == ""
}
}
let tf = UITextField()
tf.text = " \n \n "
tf.isEmpty // true
If you have several textfields that you want to check, you could put them all in a guard statement
guard let text1 = textField1.text, let text2 = textField2.text, let text3 = textField3.text, !text1.isEmpty, !text2.isEmpty, !text3.isEmpty else {
//error handling
return
}
//Do stuff
I like to validate each text field depending on the content that should be provided by the user, i.e. emailTextField should contain a valid email address etc. While Ashley Mills answer is convenient, if you regard whitespace " " as text this will return false.
In your case, since you need to validate multiple text fields in the same way, why not extend UITextField as Ashley did with a static class method that can validate each text field passed as an array, in addition to this have other validation methods for each type of text field. Instead of returning a Boolean value I've learned to use guard instead. In this way guard let can be used to check if the validation fails (is nil) and execute the proper code, such as displaying a prompt to the user, or otherwise continue execution.
UITextFieldExtension.swift
import Foundation
import UIKit
extension UITextField {
/// Validates all text field are non-nil and non-empty, Returns true if all fields pass.
/// - Returns: Bool
static func validateAll(textFields:[UITextField]) -> Bool {
// Check each field for nil and not empty.
for field in textFields {
// Remove space and new lines while unwrapping.
guard let fieldText = field.text?.trimmingCharacters(in: .whitespacesAndNewlines) else {
return false
}
// Are there no other charaters?
if (fieldText.isEmpty) {
return false
}
}
// All fields passed.
return true
}
//A function that validates the email address...
func validateEmail(field: UITextField) -> String? {
guard let trimmedText = field.text?.trimmingCharacters(in: .whitespacesAndNewlines) else {
return nil
}
//email addresses are automatically detected as links in i0S...
guard let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return nil
}
let range = NSMakeRange(0, NSString(string: trimmedText).length)
let allMatches = dataDetector.matches(in: trimmedText,
options: [],
range: range)
if allMatches.count == 1,
allMatches.first?.url?.absoluteString.contains("mailto:") == true
{
return trimmedText
}
return nil
}
func validateUserName(field: UITextField) -> String? {
guard let text:String = field.text else {
return nil
}
/* 3 to 12 characters, no numbers or special characters */
let RegEx = "^[^\\d!##£$%^&*<>()/\\\\~\\[\\]\\{\\}\\?\\_\\.\\`\\'\\,\\:\\;|\"+=-]+$"
let Test = NSPredicate(format:"SELF MATCHES %#", RegEx)
let isValid = Test.evaluate(with: text)
if (isValid) {
return text
}
return nil
}
/*6 to 16 Characters */
func validatePassword(field: UITextField) -> String?{
guard let text:String = field.text else {
return nil
}
/*6-16 charaters, and at least one number*/
let RegEx = "^(?=.*\\d)(.+){6,16}$"
let Test = NSPredicate(format:"SELF MATCHES%#", RegEx)
let isValid = Test.evaluate(with: text)
if (isValid) {
return text
}
return nil
}
}
Meanwhile, elsewhere...
if (UITextField.validateAll(textFields: [emailTextField, nameTextField])) {
// Do something
}
I have created a search bar in Xcode using Swift 3. I get the data through an api. However, while I search the cursor goes away. Not only that but before I put a delay in it I could only type one letter. I'm not sure if it was my code, the async or what.
So I found this article (http://shrikar.com/swift-ios-tutorial-uisearchbar-and-uisearchbardelegate/)
The problem I'm having is this portion in Swift 3:
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
filtered = data.filter({ (text) -> Bool in
let tmp: NSString = text
let range = tmp.rangeOfString(searchText, options: NSStringCompareOptions.CaseInsensitiveSearch)
return range.location != NSNotFound
})
if(filtered.count == 0){
searchActive = false;
} else {
searchActive = true;
}
self.tableView.reloadData()
}
So I used the documentation and found:
static var caseInsensitive: NSString.CompareOptions
My question is how do I interpret this? This is supposed to mean that caseInsensitive is type String.CompareOptions?
Do I stack this on options: caseInsensitive: String.CompareOptions?
I'm so new at this. I know python and that's about it but I love learning these new languages.
Wow at least now I understand the documentation. lol I changed it to:
let range = tmp.range(of: searchText, options: NSString.CompareOptions.caseInsensitive)