Accounting for backspace in UITextField validation using shouldChangeCharactersInRange - ios

I'm attempting to put validation in a textField to ensure it only contains letters. It works until I press backspace.
As a workaround, I tried to create a NSMutableCharacterSet and use formUnionWithCharacterSet to add NSCharacterSet.letterCharacterSet(). Then I added backspace to it. That's not working either.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
// Limit to Letters & Backspace
// let charactersAllowed = NSCharacterSet.letterCharacterSet()
let charactersAllowed = NSMutableCharacterSet()
charactersAllowed.formUnionWithCharacterSet(NSCharacterSet.letterCharacterSet())
charactersAllowed.addCharactersInString("\\b")
if let rangeOfCharactersAllowed = string.rangeOfCharacterFromSet(charactersAllowed, options: .CaseInsensitiveSearch) {
return rangeOfCharactersAllowed.count == string.characters.count
} else {
displayAlert("Invalid Character", alertMessage: "Please enter only letters")
return false
}
}
I've found some old crusty answers in Objective-C, but none of them seem to work for me. Any other suggestions for going to NSMutableCharacterSet route for validation?
If it helps, I'm using the simulator and attempting to press the backspace key (delete) in the upper-right-hand corner of the keyboard.

Try this:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let notLetterSet = NSCharacterSet.letterCharacterSet().invertedSet
let textFieldInvalidCharPosition = string.rangeOfCharacterFromSet(notLetterSet)
if (textFieldInvalidCharPosition != nil) {
return false
} else {
return true
}
}

Related

iOS 13 Crash with SwipeKeyboard and textfield:shouldChangeCharactersIn:

In iOS 13, when implementing shouldChangeCharactersIn via the UITextfieldDelegate, the application crashes when using the swiping keyboard.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as NSString? {
let txtAfterUpdate = text.replacingCharacters(in: range, with: string)
textField.text = txtAfterUpdate
}
return false
}
Is this an Apple bug?
I was able to reproduce this - if you mutate the state of the text on a UITextField during swipe entry - and only during swipe entry, it'll attempt to reinsert the swiped content (even if you return false), which retriggers your delegate event, which kicks off the recursive cycle.
It's a bit of a hack but you could catch it with something like
private var lastEntry: String?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.count > 1 && string == lastEntry { // implies we're swiping or pasting
print("Caught unwanted recursion")
return
}
lastEntry = string
if let text = textField.text as NSString? {
let txtAfterUpdate = text.replacingCharacters(in: range, with: string)
textField.text = txtAfterUpdate
}
return false
}
It'll stop users from pasting/swiping the same thing twice in a row, but at least it'll let them swipe while Apple fixes their problem.
I used UIPasteboard to identify when the user is pasting and then leave the text as the user entered using the swipe like this:
public func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
//check if the user used swipe keyboard
if string.count > 1 && string != UIPasteboard.general.string ?? "" {
return true
}
//do the text treatment
return false
}
I also realized that the TextField only accepts static strings when using swipe keyboard.
Hope it Helps.
Before setting text you can reset delegate and after set it to self again.
But this solution has one problem if textfield is empty - text will be doubled.
Му code example:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentText: String = textField.text ?? ""
if #available(iOS 13, *) {
textField.delegate = nil
let resultText = editedText
textField.text = resultText
if currentText.isEmpty, textField.text != resultText {
textField.text = resultText
}
textField.delegate = self
} else {
textField.text = input.result
}
return false
}

allow english and arabic numbers input UITextField swift

I write code that only allow english number in UITextField and I want to check arabic numbers and allow thats.
this is my code :
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let textString = "0123456789_"
let cs : NSCharacterSet = NSCharacterSet(charactersInString: textString).invertedSet
let filter = string.componentsSeparatedByCharactersInSet(cs).joinWithSeparator("") as String
return string == filter
}
in my code I want allow english number and remove button now I want add arabic numbers in it.
I try with this : let textString = "0123456789_۰۱۲۳۴۵۶۷۸۹" but this not working!
I got it !!! hahahahahaha :D
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
// let textString = "0123456789_۰۱۲۳۴۵۶۷۸۹"
let cs : NSCharacterSet = NSCharacterSet.decimalDigitCharacterSet().invertedSet
if string.rangeOfCharacterFromSet(cs) == nil {
return true
} else {
return false
}
}

How to enable a clear button action in UITextField (swift)?

I'm pretty new to Swift and I have a problem that I don't know how to solve. So, I have a UITextField where I have a limit of 5 characters max in the text field and I have no problems with stopping on fifth character, but the problem is that I can't clear text, because clear button probably consider to be a character in iOS.
Anyone can help me to solve this problem?
Here is my code:
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool{
if let count = textField.text?.characters.count {
if count < 5 {
print("\(count)")
return true
}
else {
return false
}
}
return true
}
Thanks.
You want to show the 'X' button to clear the text, right? Then use this:
textField.clearButtonMode = .WhileEditing
You need to check what the length of the text field would be, not what it currently is. Try this:
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool{
let oldText: NSString = textField.text!
let newText: NSString = oldText.stringByReplacingCharactersInRange(range, withString: string)
let count = newText.length
if count <= 5 {
print("\(count)")
return true
} else {
return false
}
}
Note: I'm not fluent in Swift. There may be a syntax error or two in this code.

How do I get the value of a UITextfield as characters are being typed in real time?

I am attempting to regulate the input of a UITextfield in real time, meaning as a user is typing. I have this character set that i need to compare to the input string, and while editing if an unwarranted character is typed in, I want to relay an alert. Here is my character set :
let acceptedChars = NSCharacterSet(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_")
now how do i capture a specific textfield in real time and track its input?
Try this:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let invalidCharacters = NSCharacterSet(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_").invertedSet
if let range = string.rangeOfCharacterFromSet(invalidCharacters, options: nil, range:Range<String.Index>(start: string.startIndex, end: string.endIndex)) {
return false
}
return true
}
You can register your textField for value change event like this
textfield.addTarget(self, action:"textFieldDidChange", forControlEvents:UIControlEvents.EditingChanged)
func textFieldDidChange(){
// put your code
}
It will work for each chracter you have been typed in real time
var strings: NSString?
class ViewController: UIViewController,UITextFieldDelegate //set your textfield delegate
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{ if(textField .isEqual(your textfield))
{
strings=string;
let acceptedChars = NSCharacterSet(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_").invertedSet;
if (strings!.rangeOfCharacterFromSet(acceptedChars.invertedSet).location != NSNotFound)
{
return true;
}
else
{
return false;
}
}
else
{
return true;
}
}
use below method
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
if textField.isEqual(<textField whose value to be copied>)
{
<TextField to be updated>.text = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
}
return true
}
An Easy Delegate method and really more efficient is:
func textFieldDidChangeSelection(_ textField: UITextField) {
print(textField.text)
}

Detect backspace Event in UITextField

I am searching for solutions on how to capture a backspace event, most Stack Overflow answers are in Objective-C but I need on Swift language.
First I have set delegate for the UITextField and set it to self
self.textField.delegate = self;
Then I know to use shouldChangeCharactersInRange delegate method to detect if a backspace was pressed is all code are in Objective-C. I need in Swift these following method as below is used.
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
const char * _char = [string cStringUsingEncoding:NSUTF8StringEncoding];
int isBackSpace = strcmp(_char, "\b");
if (isBackSpace == -8) {
// NSLog(#"Backspace was pressed");
}
return YES;
}
Swift 4.2
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if let char = string.cString(using: String.Encoding.utf8) {
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
}
}
return true
}
Older Swift version
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
println("Backspace was pressed")
}
return true
}
I prefer subclassing UITextField and overriding deleteBackward() because that is much more reliable than the hack of using shouldChangeCharactersInRange:
class MyTextField: UITextField {
override public func deleteBackward() {
if text == "" {
// do something when backspace is tapped/entered in an empty text field
}
// do something for every backspace
super.deleteBackward()
}
}
The shouldChangeCharactersInRange hack combined with an invisible character that is placed in the text field has several disadvantages:
with a keyboard attached, one can place the cursor before the invisible character and the backspace isn't detected anymore,
the user can even select that invisible character (using Shift Arrow on a keyboard or even by tapping on the caret) and will be confused about that weird character,
the autocomplete bar offers weird choices as long as there's only this invisible character,
Asian language keyboards that have candidate options based on the text field's text will be confused,
the placeholder isn't shown anymore,
the clear button is displayed even when it shouldn't for clearButtonMode = .whileEditing.
Of course, overriding deleteBackward() is a bit inconvenient due to the need of subclassing. But the better UX makes it worth the effort!
And if subclassing is a no-go, e.g. when using UISearchBar with its embedded UITextField, method swizzling should be fine, too.
Swift 5.3
In some version its changed and now it says:
When the user deletes one or more characters, the replacement string
is empty.
So answer for this:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.isEmpty {
// do something
}
return true
}
If you want to detect that some characters will be deleted
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if range.length > 0 {
// We convert string to NSString instead of NSRange to Range<Int>
// because NSRange and NSString not counts emoji as one character
let replacedCharacters = (string as NSString).substring(with: range)
}
return true
}
If you want detect backspaces even on empty textField
class TextField: UITextField {
var backspaceCalled: (()->())?
override func deleteBackward() {
super.deleteBackward()
backspaceCalled?()
}
}
Old answer
Please don't trash your code. Just put this extension somewhere in your code.
extension String {
var isBackspace: Bool {
let char = self.cString(using: String.Encoding.utf8)!
return strcmp(char, "\\b") == -92
}
}
And then just use it in your functions
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.isBackspace {
// do something
}
return true
}
In Swift 3
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let char = string.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
}
return true
}
:)
Try this
public func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if(string == "") {
print("Backspace pressed");
return true;
}
}
Note: You can return "true" if you want to allow backspace. Else you can return "false".
If u need detect backspace even in empty textField (for example in case if u need auto switch back to prev textField on backSpace pressing), u can use combination of proposed methods - add invisible sign and use standard delegate method textField:shouldChangeCharactersInRange:replacementString: like follow
Create invisible sign
private struct Constants {
static let InvisibleSign = "\u{200B}"
}
Set delegate for textField
textField.delegate = self
On event EditingChanged check text and if needed add invisible symbol like follow:
#IBAction func didChangeEditingInTextField(sender: UITextField) {
if var text = sender.text {
if text.characters.count == 1 && text != Constants.InvisibleSign {
text = Constants.InvisibleSign.stringByAppendingString(text)
sender.text = text
}
}
}
Add implementation of delegate method textField:shouldChangeCharactersInRange:replacementString:
extension UIViewController : UITextFieldDelegate {
// MARK: - UITextFieldDelegate
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
if var string = textField.text {
string = string.stringByReplacingOccurrencesOfString(Constants.InvisibleSign, withString: "")
if string.characters.count == 1 {
//last visible character, if needed u can skip replacement and detect once even in empty text field
//for example u can switch to prev textField
//do stuff here
}
}
}
return true
}
}
I implemented this feature:
And in the case where the last textFiled is empty, I just want to switch to the previous textFiled. I tried all of the answers above, but no one works fine in my situation. For some reason, if I add more logic than print in isBackSpace == -92 parentheses block this method just stopped work...
As for me the method below more elegant and works like a charm:
Swift
class YourTextField: UITextField {
// MARK: Life cycle
override func awakeFromNib() {
super.awakeFromNib()
}
// MARK: Methods
override func deleteBackward() {
super.deleteBackward()
print("deleteBackward")
}
}
Thanks #LombaX for the answer
Swift 4
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("Backspace was pressed")
return false
}
}
Swift 4
I find the comparison using strcmp irrelevant. We don't even know how strcmp is operating behind the hoods.In all the other answers when comparing current char and \b results are -8 in objective-C and -92 in Swift. I wrote this answer because the above solutions did not work for me. ( Xcode Version 9.3 (9E145) using Swift 4.1 )
FYI : Every character that you actually type is an array of 1 or more elements in utf8 Encoding. backSpace Character is [0]. You can try this out.
PS : Don't forget to assign the proper delegates to your textFields.
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let char = string.cString(using: String.Encoding.utf8)!
if (char.elementsEqual([0])) {
print("Backspace was pressed")
}
else {
print("WHAT DOES THE FOX SAY ?\n")
print(char)
}
return true
}
Swift 4
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//MARK:- If Delete button click
let char = string.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
return true
}
}
Swift 4: If the user presses the backspace button, string is empty so this approach forces textField to only accept characters from a specified character set (in this case utf8 characters) and backspaces (string.isEmpty case).
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.cString(using: String.Encoding.utf8) != nil {
return true
} else if string.isEmpty {
return true
} else {
return false
}
}
Swift 5: Per the text view delegate documentation, if the replacement text returned by the shouldChangeTextIn method is empty, the user pressed the backspace button. If the range upper bound is greater than the range lower bound (or range count is 1 or more), text should be deleted. If the range upper and lower bounds are the same (or both equal 0), then there is no text to delete (a range of length 0 to be replaced by nothing). I tested, and this will get called even on an empty textfield.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard text.isEmpty else { return true } // No backspace pressed
if (range.upperBound > range.lowerBound) {
print("Backspace pressed")
} else if (range.upperBound == range.lowerBound) {
print("Backspace pressed but no text to delete")
if (textView.text.isEmpty) || (textView.text == nil) {
print("Text view is empty")
}
}
return true
}
I came here looking for an answer of how to detect deletions. I wanted to know when the UITextView was empty after a user taps delete. As I was testing, I realized I needed to capture whole word deletions and cuts as well. Here's the answer I came up with.
extension SomeViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let deleteTapped = text == ""
let cursorAtBeginning = range.location == 0
let deletedCharactersEqualToTextViewCount = range.length == textView.text.count
let everythingWasDeleted = deleteTapped && cursorAtBeginning && deletedCharactersEqualToTextViewCount
if everythingWasDeleted {
// Handle newly empty view from deletion
} else {
// Handle other situation
}
return true
}
}
Thanks for all the previous helpful answers. I hope this helps someone.

Resources