I have been trying to implement a regex validation for a given city name.
My regex should match cities names like:
NY
San Francisco
München
København
Saint-Tropez
St. Lucia
So far I've searched the web for such-like regex but I've been having a hard time trying to implement it in swift.
this is the regex I've come up with, it seems to work in other languages but not in swift:
^[a-zA-Z\\u0080-\\u024F\\s\\/\\-\\)\(\`\.\"\']+$
Also as additional info, I'd like to implement it in the UITextField delegate method:
shouldChangeCharactersIn
something like this:
override func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
return validNameRegex.matches(string)
}
using a NSRegularExpression Extension:
extension NSRegularExpression {
convenience init(_ pattern: String) {
do {
try self.init(pattern: pattern)
} catch {
preconditionFailure("Illegal regular expression: \(pattern).")
}
}
func matches(_ string: String) -> Bool {
let range = NSRange(location: 0, length: string.utf16.count)
return firstMatch(in: string, options: [], range: range) != nil
}
}
Hope someone can help me out. Thanks in advance, and happy new year :)
In the end I ended up using this regex:
^[a-zA-Z\u{0080}-\u{024F}\\s\\/\\-\\)\\(\\`\\.\\\"\\']*$
the main problem was scaping the "\" characters and the { } in the unicode
Related
My current code is:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == self.transferBalanceTextField {
if transferBalanceTextField.text! > currentBalance.text! {
return false
}
}
return true
}
But there are issues with this because if I type in a 6 first and the currentBalance is 582, it doesn't let me type in another digit because it believes the 6 is greater then 582, HELP!
Convert string to Ints in your case, and check it:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == self.transferBalanceTextField {
if let str1 = transferBalanceTextField.text,
let int1 = Int(str1),
let str2 = currentBalance.text,
let int2 = Int(str2),
int1 > int2 {
return false
}
}
return true
}
Avoid force unwrap as much as possible - it'll help you a lot when code will be big.
UPDATE after comments
Simple code above only shows converting Strings to Ints and testing it. I thought it's clear, you see it and understand.
So - if you want full test according to your case - you have also take into account what exactly this function check (should in its name) and how to deal with it.
You have two states of your field - current and next one after possible update (should you change the text in field on user input or not?). Above simple code only test current state before new user input. You have to do checking with future possible text value when new user input already accepted to the field.
For example how it work now using code above (lets user already input 100 in first field and have 500 in another field):
user press "0" - want to enter 1000
you got call of this method
you check old value - 100 - against another value and decide should it be updated or not
method return true - (should change)
and it change it - field become 1000
after it you can't update field - method now will always return false (1000 is over 500)
What you need to do:
create string what should be in field after user input (in my example it must be 1000) - you have current value + new input + range on this new input
and check against it and another value.
Why it's matter - in this method you can check all cases of user input - simple input one char by one, paste from clipboard, replacing some characters, input in middle of string, full clear and so on - and all this must be done without and before real field visual updating.
Here is full code, you should use for full your case:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == self.loginField {
if let str1 = loginField.text,
let str2 = passwordField.text,
let rangeExpr = Range(range, in: str1) {
let strFuture = str1.replacingCharacters(in: rangeExpr, with: string)
if let int1 = Int(strFuture),
let int2 = Int(str2),
int1 > int2 {
return false
}
}
}
return true
}
main change is in line
let strFuture = str1.replacingCharacters(in: rangeExpr, with: string)
Please, read methods you use, how it's work, and what your should do for it carefully - and you will get your progress much faster.
I'm writing code restricting the user's ability to enter alphabetic texts and repeating decimal into the textField.
In the code below my text field were able to accept only user's numeric input and so does the code I commented out however, the only difference is the user can't delete after he/she type in a number.
Whereas, the user were able to delete after he/she type in the number if I replace the current code with the commented code.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeperator = textField.text?.range(of: ".")
let replacementTextHasDecimalSeperator = string.range(of: ".")
// let replacementTextCharacterSet = CharacterSet(charactersIn: string)
// let replacementTextIsAlphabetic = (replacementTextCharacterSet.isDisjoint(with: CharacterSet.letters) == false)
///This means an element in set A intersect with an element in set B
let replacementTextCharacterSet = CharacterSet(charactersIn: string)
let replacementTextIsAlphabetic = (replacementTextCharacterSet.isSubset(of: CharacterSet.letters) == true)
if (existingTextHasDecimalSeperator != nil && replacementTextHasDecimalSeperator != nil) || replacementTextIsAlphabetic {
return false
} else {
return true
}
The code that was commented out above work as intended however, the current code isn't and I failed to understand why.
Can you please explain to me the reason why I can't delete what I input afterward.
If I use .isSubset = true the logic seems the same to me.
you need to handle backspace in your shouldChangeCharactersIn function
https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619599-textfield
string
The replacement string for the specified range. During typing,
this parameter normally contains only the single new character that
was typed, but it may contain more characters if the user is pasting
text. When the user deletes one or more characters, the replacement
string is empty.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if (string.isEmpty){
print("Backspace")
}
return true
}
I am trying to make the text box only take alpha numeric input only, but how do I do it in a function already without having to create another function or protocol to do so? I am only 4 weeks into learning swift, I am not sure how to use CharacterSet or Range, but I need to only take A-Z and 0-9, BUT with that input I have to be able to shift through it in that order, so if I shift it by 2 and i am on z, it will give me 1, or If i am on a and i shift by -1 it will go to 9. I have to use the protocol that I already have in place which has 2 functions, encrypt and decrypt and takes in 2 parameters, a string for the input and another string for the int to shift it by but that too takes a string, I convert that to an int to use. I am just lost on how to restrict input in the functions and shift through the indices with the int given.
protocol CipherProtocol {
func encrypt (plaintext: String, secret: String) -> String
func decrypt (output: String, secret: String) -> String
}
struct AlphanumericCesarCipher: CipherProtocol {
func encrypt(plaintext: String, secret: String) ->
String {
guard let secret = UInt32(secret) else {
return "error"
}
var encoded = ""
for character in plaintext {
if let alpha = ["A","B","C","D","E","F","G","H","I","J","K","L","M",
"N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
"0","1","2","3","4","5","6","7","8","9"] {
}
guard let firstUnicodeScalar = character.alpha.first else {
return "error"
}
let unicode = firstUnicodeScalar.value
let shiftedUnicode = unicode + secret
let shiftedCharacter = String(UnicodeScalar(UInt8(shiftedUnicode))) //what does this line mean
encoded += shiftedCharacter
}
return encoded
}
func decrypt(output: String, secret: String) -> String {
return ""
}
}
I think what you need is using of 'Regular Expressions'. In this case your text input will only accept alphanumeric or any set of characters that you specify.
Try this code in your view controller to only allow a-z,A-Z,0-9 characters as input. Then you can use your logic to shift or other tasks you need.
override func viewDidLoad() {
super.viewDidLoad()
txt_input.delegate = self
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
do {
let text = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let regex = try NSRegularExpression(pattern: "^([a-zA-Z0-9])$", options: [])
if regex.firstMatch(in: text, options: [], range: NSMakeRange(0, text.count)) != nil {
return true
}
}
catch {
print("ERROR")
}
return false
}
I am trying to create a regex which can be used for a name.
static let nameRegex = try! NSRegularExpression(pattern: "^[a-zA-Z][A\\p{L}z'\\s]{0,19}$", options: [])
I am facing 2 issues:
If I type apostrophe, it does not let me type anything further.
I am unable to delete the first character on backspace in textfield because of [a-zA-Z] in the regex.
I am trying to make it such that it should have a limit of 20 characters, should start with an alphabet, and should allow special characters to accept names such as : José, names with apostrophes too.
I am checking the regex like this:
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let textFieldText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let filtered: [NSTextCheckingResult] = Constant.Regex.nameRegex.matches(in: textFieldText, options: [], range: NSMakeRange(0, textFieldText.count))
return filtered.count == 1
}
}
Any help will be appreciated!
You may use
"^(?!\\P{L})[\\p{L}'\\s]{0,20}$"
The pattern matches a string that fully matches the following patterns:
^ - start of string
(?!\\P{L}) - a negative lookahead that fails the match if the next char is not a non-letter char (it requires a letter to the right of the current location or end of string)
[\\p{L}'\\s]{0,20} - 0 to 20 letters, ' or whitespaces
$ - end of string.
In the function textField, the second part (starting with 'let ...') performs perfectly by finding whenever more than one decimal point has been attempted to be entered into a textField, and does not allow more than one decimal point to be displayed in a textField.
The problem is that in the first part. Here it tries to first find if an alphabetic character has been typed. It does recognize that happening, and exits returning 'false'.
Unlike the 'repeated decimal finding part of the code, the textField displays the bad character rather than skipping it.
Maybe I don't understand how this delegated method is intended to work. I though the method somehow when it returns false it prevents that character from being added to the textField display.
func textField(_ textField:UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
for scalar in (textField.text?.unicodeScalars)! {
if NSCharacterSet.letters.contains(scalar) {
print("letter scalar found: \(scalar.value), false")
return false
}
}
let existingTextHasDecimalSeperator = textField.text?.range(of: ".") // search textField for '.', can be 'nil'
let replacementTextHasDecimalSeperator = string.range(of: ".") // next character is '.', can be 'nil'
if existingTextHasDecimalSeperator != nil, replacementTextHasDecimalSeperator != nil {
return false
} else {
return true
}
}
It's because you're searching the existing textField.text, not the new character (replacementString) for alpha characters.
Change (textField.text?.unicodeScalars)! to string.unicodeScalars and you should be set.
func textField(_ textField:UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
for scalar in string.unicodeScalars {
if NSCharacterSet.letters.contains(scalar) {
print("letter scalar found: \(scalar.value), false")
return false
}
}
let existingTextHasDecimalSeperator = textField.text?.range(of: ".") // search textField for '.', can be 'nil'
let replacementTextHasDecimalSeperator = string.range(of: ".") // next character is '.', can be 'nil'
if existingTextHasDecimalSeperator != nil, replacementTextHasDecimalSeperator != nil {
return false
} else {
return true
}
}
The suggestion of replacing the search from textField to string works! Here is a listing of the swift 3.0 version that works.