Question

I have a Verification ViewController, I get 4 digit verification code by SMS and I need to enter those code to login, I have created the ViewController like this

As you can see four UITextFields, I need to allow only single digit for each UITextField,

What I tried: I was trying to use shouldChangeCharactersInRange:method: , but its not getting called, I don't know what's wrong, I think because UITextFields are in UITableView so it is not working.

Was it helpful?

Solution

You can change the text field like this by using the delegate function of the text field. Initially, you need to set the delegate and the tag of each text field.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if ((textField.text.length >= 1) && (string.length > 0))
    {
        NSInteger nextTag = textField.tag + 1;
        // Try to find next responder
        UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
        if (! nextResponder)
            nextResponder = [textField.superview viewWithTag:1];

        if (nextResponder)
           // Found next responder, so set it.
           [nextResponder becomeFirstResponder];

        return NO;
    }
    return YES;
}

Swift 2

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    // On inputing value to textfield
    if (textField.text?.characters.count < 1  && string.characters.count > 0){
        let nextTag = textField.tag + 1;

        // get next responder
        var nextResponder = textField.superview?.viewWithTag(nextTag);

        if (nextResponder == nil){
            nextResponder = textField.superview?.viewWithTag(1);
        }
        textField.text = string;
        nextResponder?.becomeFirstResponder();
        return false;
    }
    else if (textField.text?.characters.count >= 1  && string.characters.count == 0){
        // on deleting value from Textfield
        let previousTag = textField.tag - 1;

        // get next responder
        var previousResponder = textField.superview?.viewWithTag(previousTag);

        if (previousResponder == nil){
            previousResponder = textField.superview?.viewWithTag(1);
        }
        textField.text = "";
        previousResponder?.becomeFirstResponder();
        return false;
    }
    return true;
}

Swift 4

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    if textField.text!.count < 1  && string.count > 0{
        let nextTag = textField.tag + 1

        // get next responder
        var nextResponder = textField.superview?.viewWithTag(nextTag)

        if (nextResponder == nil){

            nextResponder = textField.superview?.viewWithTag(1)
        }
        textField.text = string
        nextResponder?.becomeFirstResponder()
        return false
    }
    else if textField.text!.count >= 1  && string.count == 0{
        // on deleting value from Textfield
        let previousTag = textField.tag - 1

        // get next responder
        var previousResponder = textField.superview?.viewWithTag(previousTag)

        if (previousResponder == nil){
            previousResponder = textField.superview?.viewWithTag(1)
        }
        textField.text = ""
        previousResponder?.becomeFirstResponder()
        return false
    }
    return true

}

OTHER TIPS

Use this code if you don't want to work with tag and it works better then above

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        // On inputing value to textfield
        if ((textField.text?.characters.count)! < 1  && string.characters.count > 0){
            if(textField == txtOne)
            {
                txtTwo.becomeFirstResponder()
            }
            if(textField == txtTwo)
            {
                txtThree.becomeFirstResponder()
            }
            if(textField == txtThree)
            {
                txtFour.becomeFirstResponder()
            }

            textField.text = string
            return false
        }
        else if ((textField.text?.characters.count)! >= 1  && string.characters.count == 0){
            // on deleting value from Textfield
            if(textField == txtTwo)
            {
                txtOne.becomeFirstResponder()
            }
            if(textField == txtThree)
            {
                txtTwo.becomeFirstResponder()
            }
            if(textField == txtFour)
            {
                txtThree.becomeFirstResponder()
            }
            textField.text = ""
            return false
        }
        else if ((textField.text?.characters.count)! >= 1  )
        {
            textField.text = string
            return false
        }
        return true
    }

I have taken one Hidden text field & four imageViews for that with two images. One for Blank and other for Bullet same as iOS default.

Also set tags for four imageviews.

On Load set Focus for Pin Code

- (void)startPinCode
{
    txtPinCodeLockDigits.text = @"";

    for (int i = 1; i <= 4; i++) {

       UIImageView *img = (UIImageView *)[self.view viewWithTag:i];
       [img setImage:[UIImage imageNamed:@"Img_BG_PinCode.png"]];
    }

    [txtPinCodeLockDigits becomeFirstResponder];
}

Then change imageview's images as per user input and only allow four characters

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString *result = [textField.text stringByReplacingCharactersInRange:range withString:string];
    textField.text = result;

    for (int i = 1; i <= 4; i++) {

       UIImageView *img = (UIImageView *)[self.view viewWithTag:i];
       if (i <= [result length])
           [img setImage:[UIImage imageNamed:@"Img_BG_PinCode_Filled.png"]];
       else
           [img setImage:[UIImage imageNamed:@"Img_BG_PinCode.png"]];
     }

     NSLog(@"Result :: %@", result);

     if ([result length] == 4) {
        [self performSelector:@selector(keyGenerationForApplication:) withObject:result afterDelay:0.2];
     }

     return NO;
}

After Four characters call function for generated PIN Code and store it in User Defaults same as iOS default PIN settings

- (void)keyGenerationForApplication:(NSString *)pinCode
{
     int appCode = [pinCode intValue];
     [DefaultsValues setIntegerValueToUserDefaults:appCode ForKey:PIN_LOCK_PATTERN];
}

Here, you can again call StartPinCode method for re-confirming code.

Hopefully, it'll help you.
Thanks

It can be achieve using UITextField delegate & by setting Tag for each Textfield in increasing order (say 1 - 4), below is the delegate handler to solve the issue.

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        // On inputing value to textfield
        if (textField.text?.characters.count < 1  && string.characters.count > 0){
            let nextTag = textField.tag + 1;

            // get next responder
            var nextResponder = textField.superview?.viewWithTag(nextTag);

            if (nextResponder == nil){
                nextResponder = textField.superview?.viewWithTag(1);
            }
            textField.text = string;
            nextResponder?.becomeFirstResponder();
            return false;
        }
        else if (textField.text?.characters.count >= 1  && string.characters.count == 0){
            // on deleteing value from Textfield
            let previousTag = textField.tag - 1;

            // get next responder
            var previousResponder = textField.superview?.viewWithTag(previousTag);

            if (previousResponder == nil){
                previousResponder = textField.superview?.viewWithTag(1);
            }
            textField.text = "";
            previousResponder?.becomeFirstResponder();
            return false;
        }
        return true;
    }

swift 2.3

      class BankDepositsWithOTPVC: UIViewController {

            let limitLength = 1  

        override func viewDidLoad() {
            super.viewDidLoad()
        }


        }

        // MARK: Textfield Validator

        extension BankDepositsWithOTPVC : UITextFieldDelegate {


            func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
                // On inputing value to textfield
                if (textField.text?.characters.count < 1  && string.characters.count > 0){
                    let nextTag = textField.tag + 1;

                    // get next responder
                    let nextResponder = textField.superview?.viewWithTag(nextTag);

                    if (nextResponder == nil){
                        textField.resignFirstResponder()
                      //  nextResponder = textField.superview?.viewWithTag(1);
                    }
                    textField.text = string;
                    nextResponder?.becomeFirstResponder();
                    return false;

                }else  if (textField.text?.characters.count >= 1  && string.characters.count > 0){
                                    // maximum 1 digit

textField.text = "";

                    let nextTag = textField.tag + 1;

                    // get next responder
                    let nextResponder = textField.superview?.viewWithTag(nextTag);

                    if (nextResponder == nil){
                        textField.resignFirstResponder()
                        //  nextResponder = textField.superview?.viewWithTag(1);
                    }
                    textField.text = string;
                    nextResponder?.becomeFirstResponder();
                    return false;
                }
                else if (textField.text?.characters.count >= 1  && string.characters.count == 0){
                    // on deleteing value from Textfield
                    let previousTag = textField.tag - 1;

                    // get next responder
                    var previousResponder = textField.superview?.viewWithTag(previousTag);

                    if (previousResponder == nil){
                        previousResponder = textField.superview?.viewWithTag(1);
                    }
                    textField.text = "";
                    previousResponder?.becomeFirstResponder();
                    return false;
                }
                //return true;

                guard let text = textField.text else { return true }
                let newLength = text.characters.count + string.characters.count - range.length
                return newLength <= limitLength

            }

        }

Objective-C

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{

    if ((textField.text.length < 1) && (string.length > 0))
    {

        NSInteger nextTag = textField.tag + 1;
        UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
        if (! nextResponder){
            [textField resignFirstResponder];
        }
        textField.text = string;
        if (nextResponder)
            [nextResponder becomeFirstResponder];

        return NO;

    }else if ((textField.text.length >= 1) && (string.length > 0)){
        //FOR MAXIMUM 1 TEXT

        NSInteger nextTag = textField.tag + 1;
        UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
        if (! nextResponder){
            [textField resignFirstResponder];
        }
        textField.text = string;
        if (nextResponder)
            [nextResponder becomeFirstResponder];

        return NO;
    }
    else if ((textField.text.length >= 1) && (string.length == 0)){
        // on deleteing value from Textfield

        NSInteger prevTag = textField.tag - 1;
        // Try to find prev responder
        UIResponder* prevResponder = [textField.superview viewWithTag:prevTag];
        if (! prevResponder){
            [textField resignFirstResponder];
        }
        textField.text = string;
        if (prevResponder)
            // Found next responder, so set it.
            [prevResponder becomeFirstResponder];

        return NO;
    }

      return YES;
}

Modified Anurag Soni's answer in Swift 3.

  • It assumes you have outlet collection named textFields and the text fields have ordered tags set
  • It adds case when there's already some digit in text field and when user types something new - the digit is replaced
  • Input is restricted to digits only
  • It prevents from pasting more than one digit

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        // Restrict to only digits
        let aSet = NSCharacterSet(charactersIn:"0123456789").inverted
        let compSepByCharInSet = string.components(separatedBy: aSet)
        let numberFiltered = compSepByCharInSet.joined(separator: "")
    
        if string != numberFiltered {
            return false
        }
    
        // Get the unwrapped text
        guard let text = textField.text else {
            return false
        }
    
        if (text.characters.count < 1  && string.characters.count == 1) {
            // New value to empty text field
            textField.text = string
    
            // Next responder
            if let someTextField = (textFields.filter { $0.tag == textField.tag + 1 }).first {
                someTextField.becomeFirstResponder()
            } else {
                view.endEditing(true)
            }
    
            return false
        } else if (text.characters.count >= 1  && string.characters.count == 0){
            // On deleting value from Textfield
            textField.text = ""
    
            // Previous responder
            if let someTextField = (textFields.filter { $0.tag == textField.tag - 1 }).first {
                someTextField.becomeFirstResponder()
            } else {
                view.endEditing(true)
            }
    
            return false
        } else if string.characters.count == 1 {
            // There's already some digit in text field
            // Replace it with new one
            textField.text = string
    
            // Next responder
            if let someTextField = (textFields.filter { $0.tag == textField.tag + 1 }).first {
                someTextField.becomeFirstResponder()
            } else {
                view.endEditing(true)
            }
        }
    
        return false
    }
    

Swift 4

Inspired by @Anurag Soni and @Varun Naharia answers

Variant A

extension EnterConfirmationCodeTextField: UITextFieldDelegate {

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard let textFieldCount = textField.text?.count else { return false }

        // Сlosure
        let setValueAndMoveForward = {
            textField.text = string
            let nextTag = textField.tag + 1
            if let nextResponder = textField.superview?.viewWithTag(nextTag) {
                nextResponder.becomeFirstResponder()
            }
        }

        // Сlosure
        let clearValueAndMoveBack = {
            textField.text = ""
            let previousTag = textField.tag - 1
            if let previousResponder = textField.superview?.viewWithTag(previousTag) {
                previousResponder.becomeFirstResponder()
            }
        }

        if textFieldCount < 1 && string.count > 0 {

            setValueAndMoveForward()

            if textField.tag == 4 {
                print("Do something")
            }

            return false

        } else if textFieldCount >= 1 && string.count == 0 {

            clearValueAndMoveBack()
            return false

        } else if textFieldCount >= 1 && string.count > 0 {

            let nextTag = self.tag + 1
            if let previousResponder = self.superview?.viewWithTag(nextTag) {
                previousResponder.becomeFirstResponder()

                if let activeTextField = previousResponder as? UITextField {
                    activeTextField.text = string
                }
            }

            return false
        }

        return true

    }

}

Variant B (a little bit another behavior):

extension EnterConfirmationCodeTextField: UITextFieldDelegate {

        func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            guard let textFieldCount = textField.text?.count else { return false }

            // Сlosure
            let setValueAndMoveForward = {
                textField.text = string
                let nextTag = textField.tag + 1
                if let nextResponder = textField.superview?.viewWithTag(nextTag) {
                    nextResponder.becomeFirstResponder()
                }
            }

            // Сlosure
            let clearValueAndMoveBack = {
                textField.text = ""
                let previousTag = textField.tag - 1
                if let previousResponder = textField.superview?.viewWithTag(previousTag) {
                    previousResponder.becomeFirstResponder()
                }
            }

            if textFieldCount < 1 && string.count > 0 {

                setValueAndMoveForward()

                if textField.tag == 4 {
                    print("Do something")
                }

                return false

            } else if textFieldCount >= 1 && string.count == 0 {

                clearValueAndMoveBack()
                return false

            } else if textFieldCount >= 1 {

                setValueAndMoveForward()
                return false
            }

            return true

        }

 }

Also, I implemented this feature:

enter image description here

In the case where the last textFiled is empty, I just want to switch to the previous textFiled. I tried all this methods. But as for me the method below more elegant and works like a charm:

Variant A

class EnterConfirmationCodeTextField: UITextField {

    // MARK: Life cycle

    override func awakeFromNib() {
        super.awakeFromNib()

        delegate = self
    }

    // MARK: Methods

    override func deleteBackward() {
        super.deleteBackward()

        let previousTag = self.tag - 1
        if let previousResponder = self.superview?.viewWithTag(previousTag) {
            previousResponder.becomeFirstResponder()

            if let activeTextField = previousResponder as? UITextField {
                if let isEmpty = activeTextField.text?.isEmpty, !isEmpty {
                    activeTextField.text = String()
                }
            }
        }
    }

}

Variant B (a little bit another behavior):

class EnterConfirmationCodeTextField: UITextField {

    // MARK: Life cycle

    override func awakeFromNib() {
        super.awakeFromNib()

        delegate = self
    }

    // MARK: Methods

    override func deleteBackward() {
        super.deleteBackward()

        let previousTag = self.tag - 1
        if let previousResponder = self.superview?.viewWithTag(previousTag) {
            previousResponder.becomeFirstResponder()
        }
    }

}

Assign EnterConfirmationCodeTextField for each of your textFields and set they appropriate tag value.

Provide the tag to the textfield like 1,2,3,4 and directly use it

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool


    let next:NSInteger

    if string == "" {

        next = textField.tag - 1;

    }
    else{

        next = textField.tag + 1;

    }

    if (textField.text?.characters.count)! >= 1 {

        if textField.tag == 4 {

            if string == "" {

                textField.text = ""

                let temptf = self.view.viewWithTag(next) as! UITextField

                temptf.becomeFirstResponder()

                return false

            }
            else{

                if (textField.text?.characters.count)! > 1 {

                    let stringg = textField.text!
                    textField.text = stringg.replacingOccurrences(of: stringg, with: string)
                }
                return false
            }
        }
        else{
            if string == "" {

                textField.text = ""

                if next != 0 {

                    let temptf = self.view.viewWithTag(next) as! UITextField
                    temptf.becomeFirstResponder()

                }

                return false
            }

            else{

                if (textField.text?.characters.count)! > 1 {


                    let stringg = textField.text!
                    textField.text = stringg.replacingOccurrences(of: stringg, with: string)

                }

                    let temptf = self.view.viewWithTag(next) as! UITextField

                    temptf.becomeFirstResponder()

            }
        }
    }

    return true

}

enter image description here

Swift 4

when you input a number from the pin code into a text field, you should let a number be shown and then the next text field will become the first responder, so change the first responder after the code was in the text view

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let textField = textField as? PinCodeTextField else { return true }

    if string == "" {// when the backward clicked, let it go :)
        return true
    }

    // when textfield is not empty, well, next number
    if textField.pinCode.count == textField.maxCount {
        becomeFirstResponder(after: textField)
        return false
    }

    if string.count > textField.maxCharacterCount {// the max character count should be 1
        return false
    }
    return true
}
// now the text field has been filled with a number
func textFieldCotentDidChange(_ textField: UITextField) {
    print("didchange")
    guard let textField = textField as? PinCodeTextField else { return }
    if textField.pinCode.count == 0 {
        becomeFirstResponder(before: textField)
    }

    // when textfield has been filled, ok! next!
    if textField.pinCode.count == textField.maxCharacterCount {
        becomeFirstResponder(after: textField)
    }
}

for more details and the simple demo, see this link

Try this sample tutorial passcode lock

ViewController.h

#import <UIKit/UIKit.h>
@interface ViewController : UIViewController<UITextFieldDelegate>
{
 IBOutlet UITextField *txtPassword;
}
 @end

ViewController.m

- (void)viewDidLoad
{
[super viewDidLoad];
txtPassword.delegate=self;
}


- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 
{
NSUInteger newLength = [textField.text length] + [string length] - range.length;
return (newLength > 1) ? NO : YES;
}

Just use TextFieldDelegate method and check the length of the TextField after every changes

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
    if newString.characters.count == 1
    {
        nextTextField.becomeFirstResponder()
        return true
    }
    else
    {
        return false
    }
}

I was working on a similar functionality and did it in my way. Solution below. Swift 4

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

 //For clear button pressed
 //If the textfield has already text in it
 if string.count == 0 {
    textField.text = string
    return true
 }

 //For First time entry into the text field
 guard let text = textField.text, text.count <= 0  else {
    //If user enter second character
    return false
 }
 //For First time entry into the text field
 if text.count == 0  {
     textField.text = string
     textField.resignFirstResponder()
     self.nextResponde(tag: textField.tag)
     return true
  }
  return false
}

//To make the next field as responder
func nextResponde(tag: Int) {
    switch tag {
    case self.PINTextField.tag:
        guard let text = self.PINTextField1.text, text.count == 1 else {
            self.PINTextField1.becomeFirstResponder()
            return
        }
    case self.PINTextField1.tag:
        guard let text = self.PINTextField2.text, text.count == 1 else {
            self.PINTextField2.becomeFirstResponder()
            return
        }
    case self.PINTextField2.tag:
        guard let text = self.PINTextField3.text, text.count == 1 else {
            self.PINTextField3.becomeFirstResponder()
            return
        }
    default:
        let _ = tag
    }
}

Credit to @Anurag Soni, here's an alternate UX pushing onto the next field and dismissing upon completion (with layout logic also above):

-(UIView *)addSpacedTextInputToView:(UIView *)parent subtitle:(NSString *)subtitle spaces:(int)spaces atOffset:(float)offset{
    
    float parentWidth = parent.frame.size.width;
    float tfWidth = 2 * increment;
    float tfHeight = 3 * increment;
    float startX = 3 * increment;
    float remainingWidth = parentWidth - 2 * startX - spaces * tfWidth;
    float padding = remainingWidth / (float)(spaces-1);
    
    UIView * holder = [self addViewToView:parent withFrame:CGRectMake(0, offset, parentWidth, 0)];
    UIView * tfHolder = [self addViewToView:holder withFrame:CGRectMake(0, 0, parentWidth, tfHeight)];
    
    float localX = startX;
    float localY = 0.0f;
    for (int n = 0; n < spaces; n++){
        
        UITextField * tf = [UITextField new];
        tf.frame = CGRectMake(localX, localY, tfWidth, tfHeight);
        tf.textColor = [UIColor whiteColor];
        tf.font = headerFont;
        tf.textAlignment = NSTextAlignmentCenter;
        tf.tintColor = [UIColor whiteColor];
        tf.keyboardAppearance = UIKeyboardAppearanceDark;
        tf.tag = n;
        tf.delegate = self;
        [tfHolder addSubview:tf];
        
        UIView * div = [self addViewToView:tf withFrame:CGRectMake(0, tfHeight-2, tfWidth, 1)];
        div.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5f];
        
        localX += tfWidth;
        localX += padding;
        
    }
    
    localY += tfHeight;
    localY += increment;
    localY += [self addSystemLabelToView:holder colour:nil text:subtitle maxWidth:0 offset:localY].frame.size.height;
    holder.frame = CGRectMake(0, offset, parent.frame.size.width, localY);
    
    return holder;
    
}
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{

    if (string.length > 0){
       
        textField.text = string.uppercaseString; //optional to uppercase
      
        UIView * div = textField.subviews.firstObject; //change div colour
        div.backgroundColor = [UIColor whiteColor];
        
        int nextTag = (int)textField.tag + 1;
        UIResponder * nextResponder = [textField.superview viewWithTag:nextTag];
        if (!nextResponder){
            [textField resignFirstResponder];
        }
        if (nextResponder){
           [nextResponder becomeFirstResponder];
        }
    }
    
    return false;
}

try this : - For swift 3.0

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // On inputing value to textfield
    if ((textField.text?.characters.count)! < 1  && string.characters.count > 0){
        let nextTag = textField.tag + 1;

        // get next responder
        let nextResponder = textField.superview?.viewWithTag(nextTag);
        textField.text = string;

        if (nextResponder == nil){
            textField.resignFirstResponder()
        }
        nextResponder?.becomeFirstResponder();
        return false;
    }
    else if ((textField.text?.characters.count)! >= 1  && string.characters.count == 0){
        // on deleting value from Textfield
        let previousTag = textField.tag - 1;

        // get next responder
        var previousResponder = textField.superview?.viewWithTag(previousTag);

        if (previousResponder == nil){
            previousResponder = textField.superview?.viewWithTag(1);
        }
        textField.text = "";
        previousResponder?.becomeFirstResponder();
        return false;
    }
    return true;
}

converted Anurag Soni answer in swift 3.0

You just have to implement this method only.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top