iOS 6 UITextField Secure - How to detect backspace clearing all characters? - cocoa-touch

In iOS 6 if you type text into a secure text field, change to another text field, then come back to the secure text field and hit backspace, all of the characters are removed. I am fine with this happening, however, I am trying to enable/disable a button based on if this secure text field has characters in it or not. I know how to determine what characters are in the fields and if a backspace is hit but I am having trouble determining how to detect if clearing of all the characters is happening.
This is the delegate method I'm using to get the new text of a field, but, I can't seem to figure out how to get the new text (assuming the new text would just be a blank string) if a backspace is hit that clears all the characters.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
//returns the "new text" of the field
NSString * text = [textField.text stringByReplacingCharactersInRange:range withString:string];
}
Any help is much appreciated.
Thanks!

I use this solution. It does not need local variables and sets the cursor position correctly, after deleting the char.
It's a mashup of this solutions:
Backspace functionality in iOS 6 & iOS 5 for UITextfield with 'secure' attribute
how to move cursor in UITextField after setting its value
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (range.location > 0 && range.length == 1 && string.length == 0)
{
// Stores cursor position
UITextPosition *beginning = textField.beginningOfDocument;
UITextPosition *start = [textField positionFromPosition:beginning offset:range.location];
NSInteger cursorOffset = [textField offsetFromPosition:beginning toPosition:start] + string.length;
// Save the current text, in case iOS deletes the whole text
NSString *text = textField.text;
// Trigger deletion
[textField deleteBackward];
// iOS deleted the entire string
if (textField.text.length != text.length - 1)
{
textField.text = [text stringByReplacingCharactersInRange:range withString:string];
// Update cursor position
UITextPosition *newCursorPosition = [textField positionFromPosition:textField.beginningOfDocument offset:cursorOffset];
UITextRange *newSelectedRange = [textField textRangeFromPosition:newCursorPosition toPosition:newCursorPosition];
[textField setSelectedTextRange:newSelectedRange];
}
return NO;
}
return YES;
}

Finally figured it out for anyone looking to see how to determine when a backspace is going to clear all the characters of a secure UITextField:
UITextField:
self.passwordTextField
Private property (initialized to NO in init - probably not needed):
self.passwordFirstCharacterAfterDidBeginEditing
UITextFieldDelegate Methods:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
//if text is blank when first editing, then first delete is just a single space delete
if([textField.text length] == 0 && self.passwordFirstCharacterAfterDidBeginEditing)
self.passwordFirstCharacterAfterDidBeginEditing = NO;
//if text is present when first editing, the first delete will result in clearing the entire password, even after typing text
if([textField.text length] > 0 && self.passwordFirstCharacterAfterDidBeginEditing && [string length] == 0 && textField == self.passwordTextField)
{
NSLog(#"Deleting all characters");
self.passwordFirstCharacterAfterDidBeginEditing = NO;
}
return YES;
}
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
if(textField == self.passwordTextField)
{
self.passwordFirstCharacterAfterDidBeginEditing = YES;
}
}
I hope this helps someone and I also hope Apple just creates a delegate method that is called when a secure text field is cleared by a delete - this seems a big cumbersome, but it works.

Secure text fields clear on begin editing on iOS6+, regardless of whether you're deleting or entering text. Checking if the length of the replacement string is 0 works if the text field is cleared from a backspace, but annoyingly, the range and replacement string parameters to textField:shouldChangeCharactersInRange:replacementString: are incorrect if the text is cleared when entering text. This was my solution:
1) Register for textFieldTextDidChange notifications.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(textFieldTextDidChange:)
name:UITextFieldTextDidChangeNotification
object:nil];
2) Re-invoke the textField:shouldChangeCharactersInRange:replacementString: delegate method if the text field is secure and may have been cleared.
- (void)textFieldTextDidChange:(NSNotification *)notification
{
if (textField.secureTextEntry && textField.text.length <= 1) {
[self.textField.delegate textField:self.textField
shouldChangeCharactersInRange:NSMakeRange(0, textField.text.length)
replacementString:textField.text];
}
}

The accepted solution does not enable a button. It actually changes backspace behaviour on a secure textfield.
This Swift solution does solve the question by correctly informing a delegate about the actual string that will be set in the textField, including when pressing backspace. A decision can then be made by that delegate.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
var shouldChange = true
if let text = textField.text {
let oldText = text.copy()
var resultString = (text as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString
let inputString = string as NSString
if range.location > 0 && range.length == 1 && inputString.length == 0 {//Backspace pressed
textField.deleteBackward()
shouldChange = false
if let updatedText = textField.text as NSString? {
if updatedText.length != oldText.length - 1 {
resultString = ""
}
}
}
self.delegate?.willChangeTextForCell(self, string: resultString as String)
}
return shouldChange
}

I was facing the same issue, I wanted to detect when the backspace is going to clear all characters so that I can enabled disable some other buttons on screen. So this is how I achieved it.
-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
_checkForDelete = NO;
if (textField.isSecureTextEntry && textField.text.length > 0) {
_checkForDelete = YES;
}
return YES;
}
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (_checkForDelete && string.length == 0) {
_checkForDelete = NO;
//Do whatever you want to do in this case,
//because all the text is going to be cleared.
}
return YES
}
This solution is different because it lets you take action when a secure field already has some text in it and you are trying to edit it, rather in the above accepted answer, you will go in the block even if you hit backspace while already editing a field, that will not clear the whole text but only deletes one character. Using my method you have an option to take an action in this special case, but it will not impact the native flow either.

Posting my alternate solution as I had the exact problem as OP and found I did not have to do any cursor position alteration detailed in the chosen answer.
Below is the UITextFieldDelegate method, I call a custom delegate method didChangeValueForTextField as my button I want to enable is outside of this class.
Class implementing UITextFieldDelegate
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSString *newValue = [textField.text stringByReplacingCharactersInRange:range withString:string];
BOOL shouldReturn = YES;
if (range.location > 0 && range.length == 1 && string.length == 0){
[textField deleteBackward];
if ([textField.text isEmptyCheck]) { // Check if textField is empty
newValue = #"";
}
shouldReturn = NO;
}
if(self.delegate && [self.delegate respondsToSelector:#selector(didChangeValueForField:withNewValue:)]) {
[self.delegate didChangeValueForField:textField withNewValue:newValue];
}
return shouldReturn;
}
The key was detecting the difference between secure field single character deletion and secure field single character deletion that triggers field clear. Detecting this within the above delegate method was necessary.
Class containing button to enable
- (void)didChangeValueForField:(UITextField *)textField withNewValue:(NSString *)value {
// Update values used to validate/invalidate the button.
// I updated a separate model here.
// Enable button based on validity
BOOL isEnabled = [self allFieldsValid]; // Check all field values that contribute to the button being enabled.
self.myButton.enabled = isEnabled;
}

swift3.0 - it works.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == passwordTextfield {
if range.location > 0 && range.length == 1 && string.characters.count == 0 {
if let textString = textField.text {
// iOS is trying to delete the entire string
let index = textString.index(textString.endIndex, offsetBy: -1)
textField.text = textString.substring(to: index)
return false
}
}else if range.location == 0 {
return true
}else {
if let textString = textField.text {
textField.text = textString + string
return false
}
}
}
return true
}

Related

Enable button only after text fields are entered

Apple started rejecting my app (after yrs of approvals) - I have 2 text fields for a few char of last, first name, then press Go. They claim - I can't reproduce that the app crashes if you just press Go - when I run it it tries to send the request but kicks it back with an error 'No name - please reenter'. Whatever. To appease Apple I edited my text field entry method to now (see below) - but still not working. Please advise.
// Text Field methods
-(void)textFieldDidBeginEditing:(UITextField *)textField {
UIFont* boldFont = [UIFont fontWithName:#"Helvetica-Bold" size: 24];
if (textField == _lastName) {
[_lastName becomeFirstResponder];
[_lastName setFont:boldFont];
_lastName.autocorrectionType = UITextAutocorrectionTypeNo;;
_lastName.autocapitalizationType = UITextAutocapitalizationTypeNone;
_lastName.text = #"";
NSLog(#"user editing last...");
// Enable button once fields have values
if ([_lastName.text isEqual: #""] && [_firstName.text isEqual: #""])
{
_fwdButton.enabled = YES;
}
}
else {
[_firstName becomeFirstResponder];
[_firstName setFont:boldFont];
_firstName.autocorrectionType = UITextAutocorrectionTypeNo;;
_firstName.autocapitalizationType = UITextAutocapitalizationTypeNone;
_firstName.text = #"";
NSLog(#"user editing first");
// Enable button once fields have values
if ([_lastName.text isEqual: #""] && [_firstName.text isEqual: #""])
{
_fwdButton.enabled = YES;
}
}
}
I was able to add code similar to[https://stackoverflow.com/questions/2859821/disable-button-until-text-fields-have-been-entered][1]
to deal with the situation.

Prevent Users from Entering Multiple Decimals In A String

I am doing a button calculator app (still) in which users press a button, I store a value into a string to display in a text box, and then convert into floats when the user enters the = button.
Works great for the most part, except I cannot figure out how to prevent users from entering more than one decimal in a single string. I thought about passing it to a BOOL method, but none of my class materials cover methods at all.
I looked at this Stack overflow question, but trying to amend the code for my own just resulted in a whole bunch of errors. Does anyone have any advice?
-(IBAction) decimal
{
NSString *decVal =#".";
NSRange range = [decVal rangeOfString:#"."];
if (range.location==NSNotFound)
{
display.text = [display.text stringByAppendingString:decVal];
}
else
{
NSLog(#"You can't enter more than one decimal");
}
}
-(IBAction) decimal
{
static NSString *decVal =#".";
NSRange range = [display.text rangeOfString:decVal];
if (NSNotFound == range.location)
{
display.text = [display.text stringByAppendingString:decVal];
}
else
{
NSLog(#"You can't enter more than one decimal");
}
}
You have missed in one string. [decVal rangeOfString:#"."] will always return range {0,1}.
One way to handle this would be to set the delegate of the text field/view and then implement shouldChangeCharactersInRange in the delegate.
For example, you could do something like this:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
BOOL hasPeriod = ([textField.text rangeOfString:#"."].location != NSNotFound);
BOOL removingPeriod = ([[textField.text substringWithRange:range]
rangeOfString:#"."].location != NSNotFound);
BOOL addingPeriod = ([string rangeOfString:#"."].location != NSNotFound);
return (hasPeriod && addingPeriod && !removingPeriod) ? NO : YES;
}
This won't throw an error. It just won't allow a second period. You could easily add in the error message, though.

UITEXTVIEW: Get the recent word typed in uitextview

I want to get the most recent word entered by the user from the UITextView.
The user can enter a word anywhere in the UITextView, in the middle or in the end or in the beginning. I would consider it a word when the user finishes typing it and presses a space and does any corrections using the "Suggestions from the UIMenuController".
Example: User types in "kimd" in the UITextView somewhere in the middle of text, he gets a popup for autocorrection "kind" which he does. After he does that, I want to capture "kind" and use it in my application.
I searched a lot on the internet but found solutions that talk about when the user enters text in the end. I also tried detecting a space and then doing a backward search until another space after some text is found, so that i can qualify it as a word. But I think there may be better ways to do this.
I have read somewhere that iOS caches the recent text that we enter in a text field or text view. If I can pop off the top one , that's all I want. I just need handle to that object.
I would really appreciate the help.
Note: The user can enter text anywhere in UItextview. I need the most recent entered word
Thanks.
//This method looks for the recent string entered by user and then takes appropriate action.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
//Look for Space or any specific string such as a space
if ([text isEqualToString:#" "]) {
NSMutableCharacterSet *workingSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
NSRange newRange = [self.myTextView.text rangeOfCharacterFromSet:workingSet
options:NSBackwardsSearch
range:NSMakeRange(0, (currentLocation - 1))];
//The below code could be done in a better way...
UITextPosition *beginning = myTextView.beginningOfDocument;
UITextPosition *start = [myTextView positionFromPosition:beginning offset:currentLocation];
UITextPosition *end = [myTextView positionFromPosition:beginning offset:newRangeLocation+1];
UITextRange *textRange = [myTextView textRangeFromPosition:end toPosition:start];
NSString* str = [self.myTextView textInRange:textRange];
}
}
Here is what I would suggest doing, might seem a little hacky but it would work just fine:
First in .h conform to the UITextViewDelegate and set your text view's delegate to self like this:
myTextView.delegate = self;
and use this code:
- (void)textViewDidChange:(UITextView *)textView { // Delegate method called when any text is modified
if ([textView.text substringFromIndex: [textView.text length] - 1]) { // Gets last character of the text view's text
NSArray *allWords = [[textView text] componentsSeparatedByString: #" "]; // Gets the text view's text and fills an array with all strings seperated by a space in text view's text, basically all the words
NSString *mostRecentWord = [allWords lastObject]; // The most recent word!
}
}
I use this code to get the word behind the #-sign:
- (void)textViewDidChange:(UITextView *)textView {
NSRange rangeOfLastInsertedCharacter = textView.selectedRange;
rangeOfLastInsertedCharacter.location = MAX(rangeOfLastInsertedCharacter.location - 1,0);
rangeOfLastInsertedCharacter.length = 1;
NSString *lastInsertedSubstring;
NSString *mentionSubString;
if (![textView.text isEqualToString:#""]) {
lastInsertedSubstring = [textView.text substringWithRange:rangeOfLastInsertedCharacter];
if (self.startOfMention > 0 || self.startOfHashtag > 0) {
if ([lastInsertedSubstring isEqualToString:#" "] || (self.startOfMention > textView.selectedRange.location || self.startOfHashtag > textView.selectedRange.location)) {
self.startOfMention = 0;
self.lenthOfMentionSubstring = 0;
}
}
if (self.startOfMention > 0) {
self.lenthOfMentionSubstring = textView.selectedRange.location - self.startOfMention;
NSRange rangeOfMentionSubstring = {self.startOfMention, textView.selectedRange.location - self.startOfMention};
mentionSubString = [textView.text substringWithRange:rangeOfMentionSubstring];
dhDebug(#"mentionSubString: %#", mentionSubString);
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
}
}
Simple extension for UITextView:
extension UITextView {
func editedWord() -> String {
let cursorPosition = selectedRange.location
let separationCharacters = NSCharacterSet(charactersInString: " ")
let beginRange = Range(start: text.startIndex.advancedBy(0), end: text.startIndex.advancedBy(cursorPosition))
let endRange = Range(start: text.startIndex.advancedBy(cursorPosition), end: text.startIndex.advancedBy(text.characters.count))
let beginPhrase = text.substringWithRange(beginRange)
let endPhrase = text.substringWithRange(endRange)
let beginWords = beginPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
let endWords = endPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
return beginWords.last! + endWords.first!
}
}

Make portion of UITextView undeletable

I have a UITextView and need to make a specific portion un-deletable. Its the first 10 characters of the views text.
I just want it so that if the user is tapping the delete key on the keyboard it simply stops when it reaches say the 10th character in.
Edit
Let me go into a bit more detail.
Let's say the prefix is '123456789:'. I want to be able to type anywhere after this prefix, it can't be editable at all though, so '123456789:' shouldn't not be altered at all. Fichek's answer does this perfectly, however the prefix isn't always there, so how can I detect when it isn't in the textview? I thought the if statement did this but it seems not to.
You can use the delegate method textView:shouldChangeTextInRange:replacementText: To tell the text view whether to accept the delete or not.
As the documentation says:
range : The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character.
Edit
Here is an implementation where the user can't delete the the first ten characters. But he will be able to insert characters there.
- (BOOL)textView:(UITextView *)textView shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (range.length==1 && string.length == 0) {
// Deleting text
if (range.location <= 9) {
return NO;
}
}
return YES;
}
Here is an implementation where he can't modify the first ten characters at all.
- (BOOL)textView:(UITextView *)textView shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (range.location <= 9) {
return NO;
}
return YES;
}
sch's last edit makes a decent answer, but I want to offer a slightly more flexible approach.
You have to keep in mind the copy/paste system. User might select all the text in text field and try to paste in the entire value which might be perfectly acceptable, but if (range.location <= 9) { return NO; } will reject it. The way I'd do it is put together a string that would be a result of successful edit and then check if that string would start with your desired prefix.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *resultString = [textField.text stringByReplacingCharactersInRange:range withString:string];
NSLog(#"resulting string would be: %#", resultString);
NSString *prefixString = #"blabla";
NSRange prefixStringRange = [resultString rangeOfString:prefixString];
if (prefixStringRange.location == 0) {
// prefix found at the beginning of result string
return YES;
}
return NO;
}
Edit: if you want to check if the current string in text field starts with the prefix, you can use rangeOfString: the same way:
NSRange prefixRange = [textField.text rangeOfString:prefixString];
if (prefixRange.location == 0) {
// prefix found at the beginning of text field
}
for a complete solution you need to handle several cases, including the cut and paste operations that may start in the uneditable part and extend into the part which the user can edit. I added a variable to control whether or not an operation that includes the uneditable part but extends into the editable part, is valid or not. If valid, the range is adjusted to only affect the editable part.
// if a nil is returned, the change is NOT allowed
- (NSString *)allowChangesToTextView:(UITextView *)textView inRange:(NSRange)changeRange withReplacementText:(NSString *)text
immutableUpTo:(NSInteger)lastReadOnlyChar adjustRangeForEdits:(BOOL)adjustRangeForEdits;
{
NSString *resultString = #"";
NSString *currentText = textView.text;
NSInteger textLength = [currentText length];
// if trying to edit the first part, possibly prevent it.
if (changeRange.location <= lastReadOnlyChar)
{
// handle typing or backspace in protected range.
if (changeRange.length <= 1)
{
return nil;
}
// handle all edits solely in protected range
if ( (changeRange.location + changeRange.length) <= lastReadOnlyChar)
{
return nil;
}
// if the user wants to completely prevent edits that extend into the
// read only substring, return no
if (!adjustRangeForEdits)
{
return nil;
}
// the range includes read only part but extends into editable part.
// adjust the range so that it does not include the read only portion.
NSInteger prevLastChar = changeRange.location + changeRange.length - 1;
NSRange newRange = NSMakeRange(lastReadOnlyChar + 1, prevLastChar - (lastReadOnlyChar + 1) + 1);
resultString = [textView.text stringByReplacingCharactersInRange:newRange withString:text];
return resultString;
}
// the range does not include the immutable part. Make the change and return the string
resultString = [currentText stringByReplacingCharactersInRange:changeRange withString:text];
return resultString;
}
and this is how it gets called from the text view delegate method:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
// did the user press enter?
if ([text isEqualToString:#"\n"])
{
[textView resignFirstResponder];
return NO;
}
NSInteger endOfReadOnlyText = [self.spotTextLastSet length] - 1;
NSString *newText = [self allowChangesToTextView:textView inRange:range withReplacementText:text
immutableUpTo:endOfReadOnlyText adjustRangeForEdits:YES];
if (newText == nil)
{
// do not allow!
[TipScreen showTipTitle:#"Information" message:#"The first part of the text is not editable. Please add your comments at the end."
ForScreen:#"editWarning"];
return NO;
}
// lets handle the edits ourselves since we got the result string.
textView.scrollEnabled = NO;
textView.text = newText;
// move the cursor to change range start + length of replacement text
NSInteger newCursorPos = range.location + [text length];
textView.selectedRange = NSMakeRange(newCursorPos, 0);
textView.scrollEnabled = YES;
return NO;
}

Limit text field length

Please tell me how I can set the range of a text field to a maximum of six and minimum of one.
Set self as textfield's delegate and implement this method:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *newString = [textField.text stringByReplacingCharactersInRange:range withString:string];
if (newString.length < 1) {
// too short
} else if (newString.length > 6) {
// too long
}
return YES;
}
I'd suggest you to have a UILabel that will say in red text that text is too short or too long. Just preventing user from typing into textfield when they reach 6 chars or deleting last character when they change their mind is a very bad user experience.
Implement the delegate method textField:shouldReplaceCharactersInRange:replacementString: and have your implementation return NO if the resulting string would fall outside of your constraints.