Test UITextField text string to only contain alphanumeric characters - ios

Im trying to complete form validation in Swift and cant find a way of testing for only Alphanumeric characters in a UITextField.text.
Ive found NSCharacterSet to help test if at least 1 letter has been entered (so far):
#IBOutlet weak var username: UITextField!
let letters = NSCharacterSet.letterCharacterSet()
//Check username contains a letter
if (username.text!.rangeOfCharacterFromSet(letters) == nil) {
getAlert("Error", message: "Username must contain at least 1 letter")
}
Now i just need a way to validate that only numbers, letters (maybe even underscores and dashes) to be entered. Loads of stuff out there for Obj-C but I need a SWIFT solution please.
Thank you in advance.

Check if the inversion of your accepted set is present:
if username.text!.rangeOfCharacterFromSet(letters.invertedSet) != nil {
print("invalid")
}
letters should probably be alphanumericCharacterSet() if you want to include numbers as well.
If you want to accept underscores or more chars, you will probably have to create a character set by your own. But the inversion logic will stay the same.

Though it's late to answer, but this answer might be useful to someone.
This is simple and worked like a charm for me.
Swift 3:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
/// 1. replacementString is NOT empty means we are entering text or pasting text: perform the logic
/// 2. replacementString is empty means we are deleting text: return true
if string.characters.count > 0 {
var allowedCharacters = CharacterSet.alphanumerics
/// add characters which we need to be allowed
allowedCharacters.insert(charactersIn: " -") // "white space & hyphen"
let unwantedStr = string.trimmingCharacters(in: allowedCharacters)
return unwantedStr.characters.count == 0
}
return true
}
Note: This will work for pasting strings into the text field as well. Pasted string will not be displayed in text field if it contains any unwanted characters.

Related

How can I disable the return key in a UITextView based on the number of empty rows in the UITextView?

Both TikTok & Instagram (iOS) have a mechanism built into their edit profile biography code which enables the user to use the return key and create separation lines in user's profile biographies. However, after a certain number of lines returned with no text in the lines, they prevent the user from using the return key again.
How can one do this?
I understand how to prevent the return key from being used if the present line the cursor is on is empty, using the following:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard text.rangeOfCharacter(from: CharacterSet.newlines) == nil else {
return false
}
return true
Moreover, I need help figuring out how to detect, for example, that 4 lines are empty, and stating that if 4 lines are empty, preventing the user from using the return key.
This may need fine tuning for edge cases but this would be my starting point for sure. The idea is to check the last 4 inputs into the text view with the current input and decide what to do with it. This particular code would prevent the user from creating a fourth consecutive empty line.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n",
textView.text.hasSuffix("\n\n\n\n") {
return false
}
return true
}
I am not completely sure I have understood your question correctly. But let me try to help.
A UITextView has a text property that you can read. Then, if a user enters a new character/inserts new text (make sure to test this with copy-pasting text), you could check whether the last three characters of the text are newlines or not. If so, and the user is trying to add another newline, you know to return false.
This would look like this:
let lastThreeCharacters = textView.text.suffix(3)
let lastThreeAreNewlines = (lastThreeCharacters.count == 3) && lastThreeCharacters.allSatisfy( {$0.isNewline} ) // Returns true if the last 3 characters are newlines
You'd need to implement some additional checks. Is the character that's going to be inserted a newline? If the user pastes text, will the last 4 characters be newlines?
Another method would be to make use of another method of the UITextViewDelegate. You could also implement textViewDidChange(_:), which is called after a user has changed the text. Then, check if (and where) the text contains four new lines and replace them with empty characters.
This would look something like this (taken from here):
func textViewDidChange(_ textView: UITextView) {
// Avoid new lines also at the beginning
textView.text = textView.text.replacingOccurrences(of: "^\n", with: "", options: .regularExpression)
// Avoids 4 or more new lines after some text
textView.text = textView.text.replacingOccurrences(of: "\n{4,}", with: "\n\n\n", options: .regularExpression)
}

How does replacingCharacters work in a delegate?

I'm new to Swift and I'm learning about delegates:
class ZipCodeTextFieldDelegate: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var newText = textField.text! as NSString
newText = newText.replacingCharacters(in: range, with: string) as NSString // this line
return newText.length <= 5
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
}
This is basically a delegate to limit the character count to within 5. But, I'm not quite sure what role this method replacingCharacters is playing in this function. Apple documentation shows that it
Returns a new string in which the characters in a specified range of
the receiver are replaced by a given string.
which seems pretty straight forward. It replaces some range of characters with a replacement of your choice. I can see that textField has a parameter called range and replacementString, but what range is being provided and what are they being replaced with?
The main ViewController that the delegate is being used in doesn't provide either of them. It simply instantiate the delegate and applies it to the relevant textField
let zipCodeDelegate = ZipCodeTextFieldDelegate()
// MARK: Outlets
#IBOutlet weak var textField: UITextField!
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self.zipCodeDelegate
}
First, you should understand the purpose of shouldChangeCharactersIn. Whenever the user tries to change the text of a text field in some way, the text field asks its delegates whether it "should change the characters", i.e. whether it should allow the user to do the change that they are trying to do.
Note that the user can change the textfield's text in a lot of ways, not just by "entering a single character". Here's a list of possible ways (you might be able to think of others):
entering a single character, or pasting some text
deleting a single character
selecting a range of characters, and deleting it
selecting a range of characters, and entering a single character, or pasting some text. This will replace the selected range with the single character, or pasted text
Notice that all those ways are just special cases of Way 4. In all of these ways, you are just replacing a range in the textfield's text with some new text. Let me reword the first 3 ways:
replacing a zero-length range at some position with a single character, or some pasted text
replacing a one-length range of the textfield with an empty string
replacing some range of the textfield with an empty string
When the user tries to change the textfield's text in any way, the textfield will make a call like similar to this:
delegate?.textField(self, shouldChangeCharactersIn: someRange, replacementString: someString)
You should now understand what the parameters someRange and someString are for. No matter how the user changes the text, it can always be modelled as "replacing a range of text (someRange) with some other text (someString)".
So what does
newText.replacingCharacters(in: range, with: string)
do?
It simply does the replacement mentioned before. Recall that this delegate method is the textfield asking you, "should this change (modelled as a range and a replacement string) be allowed?" And to determine that, you try to do the change first. If after the change, the text field's text has a length of at most 5, then it is allowed.
The line tries to do the change.
Of course, you are not changing the textfield's text when doing this change, which is why you assign it to a variable called newText.

How to limit decimal input value in UITextField

I am currently checking my UITextField, which is set to show Numeric Keypad in shouldChangeCharactersIn to limit the input to only one decimal separator and only 2 decimal points like this (thanks to this question):
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let decimalSeparator = String(Locale.current.decimalSeparator ?? ".")
if (textField.text?.contains(decimalSeparator))! {
let limitDecimalPlace = 2
let decimalPlace = textField.text?.components(separatedBy: decimalSeparator).last
if (decimalPlace?.count)! < limitDecimalPlace {
return true
} else {
return false
}
}
}
This works great. However, it is now possible to insert whatever value the user wants, which I want to limit to a value lower than 999. I used to check the length to allow only 3 characters, but now I want to allow following values (for example):
143
542.25
283.02
19.22
847.25
But I don't want to allow:
2222
3841.11
999.99
How could I do that?
You probably need two checks:
Make sure it in the form of xxx.xx. This sort of pattern matching is often achieved by using regular expression search.
The trick here is to make sure you support all permutations with and without decimal place, where the fractional digits is two or fewer digits and the integer digits is three or fewer digits.
Try converting it to a number and check that the value is less than 999.
Thus:
let formatter = NumberFormatter()
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let candidate = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
let separator = formatter.decimalSeparator!
if candidate == "" { return true }
let isWellFormatted = candidate.range(of: "^[0-9]{1,3}([\(separator)][0-9]{0,2})?$", options: .regularExpression) != nil
if isWellFormatted,
let value = formatter.number(from: candidate)?.doubleValue,
value >= 0,
value < 999 {
return true
}
return false
}
Note:
I’m assuming you want users to be able to honor their device’s localization settings (e.g. let a German user enter 123,45 because they use , as the decimal separator).
The regular expression, "^[0-9]{1,3}([\(separator)][0-9]{0,2})?$” probably looks a little hairy if you’re not used to regex.
The ^ matches the start of the string;
The [0-9] obviously matches any digit;
The {1,3} matches between one and three integer digits;
The (...)? says “optionally, look for the following”;
Again, [0-9]{0,2} means “between zero and two fractional digits; and
The $ matches the end of the string.

How to simulate a key press when another key is pressed?

I am trying to get the delete key to do what the spacebar normally does. In other words, when the user clicks the delete in a UITextField, I want a space to appear instead.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let char = string.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Delete was pressed")
return false
}
return true
}
I know that this only disables the delete key, but how would I move forward? Also, the space would replace the selected text (there is always some text that is selected).
The textField parameter is the text field in which the user is working. You are free to do there whatever you like. So, in addition to returning false, and just before doing so, change the text of the textField to whatever you want.
If what you want to do is append a space to the text, an easy way is to fetch textField.text!, append a space to that string, and set textField.text. Or if you want to replace the current selection with a space, instead of letting it be removed, do that to textField.text, using the range parameter to tell you where the space should go.
In other words, if you know anything at all about how to work with strings, you can do that to the string that is textField.text as part of this method.
There are other ways using the UITextInput protocol, which allows you to pretend to be the keyboard. For example, just saying textField.insertText(" ") might give you desired result, but you'll have to try it and see.

Swift - Maintaining decimal point with Editing Changed

I am creating a unit converter and want all the units to update simultaneously as the user enters a value. e.g. entering 25 into the 'cm' field will automatically display 250 in the 'mm' field and 0.25 in the 'metre' field.
This is so far working great using Editing Changed event so long as the user enters whole numbers, as soon as a decimal is entered iOS automatically strips the decimal away to format the UITextField as a whole number since it is formatting after each keypress and assumes the the number entered ends with a decimal point i.e "25." which then becomes "25" instead of "25.0".
For instance entering 25.6 will display as 256.
Is there any way to prevent this automatic formatting? The only solution I have found is to link the event to Editing Did End but this is not ideal as the other units will only become updated after the user finishes entering the value and not updated automatically as I require.
The code I'm using to convert from string to double is as follows:
#IBAction func editMm(sender: AnyObject) {
var setToDouble:Double? = stringToDouble(textFieldMm.text)
valueEdited(setToBase!)
}
func stringToDouble(inputString:String) -> Double{
// this removes the comma separators which are automatically added to the UITextField during formatting
var formattedString = inputString.stringByReplacingOccurrencesOfString(",", withString: "")
var doubleResult:Double = (formattedString as NSString).doubleValue
return doubleResult
}
I've tried to catch this in the stringToDouble function but it seems the formatting happens before the value reaches that point in the code.
You can use the shouldChangeCharactersInRange function in UITextFieldDelegate. The newString will contain the current string while editing.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
println(newString)
return true
}

Resources