15
votes

Scenario: I have 4 UITextFields that only accept 1 character. Easy.

Problem: After I enter the 1 character, I want the next TextField to become active automatically without having to press next (i.e. I'm using the UIKeyboardTypeNumberPad, and theres no NEXT button. (I KNOW I can actually create a next button programmatically, but I dont want to go that far, just need the next field to become active automatically after the 1 character is entered.

#define MAX_LENGTH 1

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    NSCharacterSet *myCharSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];
    for (int i = 0; i < [string length]; i++) {
    unichar c = [string characterAtIndex:i];
        if (![myCharSet characterIsMember:c]) {
            return NO;
        }
    }
        NSUInteger newLength = [textField.text length] + [string length] - range.length;
        return (newLength > 1) ? NO : YES;
}


-(BOOL)textFieldShouldReturn:(UITextField *)textField {
    if (textField == pc1) {
        [pc2 becomeFirstResponder];
    }else if (textField == pc2) {
        [pc3 becomeFirstResponder];
    }else if (textField == pc3) {
        [pc4 becomeFirstResponder];
    }else if (textField == pc4) {
        [textField resignFirstResponder];
    }
    return YES;
}
6

6 Answers

19
votes

I arrived at a solution by modifying some code I found here: http://www.thepensiveprogrammer.com/2010/03/customizing-uitextfield-formatting-for.html

First set the your view controller to be the delegate of the textfields.

Then do something like this:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{   
    BOOL shouldProcess = NO; //default to reject
    BOOL shouldMoveToNextField = NO; //default to remaining on the current field

    int insertStringLength = [string length];
    if(insertStringLength == 0){ //backspace
        shouldProcess = YES; //Process if the backspace character was pressed
    }
    else {
        if([[textField text] length] == 0) {
            shouldProcess = YES; //Process if there is only 1 character right now
        }
    }

    //here we deal with the UITextField on our own
    if(shouldProcess){
        //grab a mutable copy of what's currently in the UITextField
        NSMutableString* mstring = [[textField text] mutableCopy];
        if([mstring length] == 0){
            //nothing in the field yet so append the replacement string
            [mstring appendString:string];

            shouldMoveToNextField = YES;
        }
        else{
            //adding a char or deleting?
            if(insertStringLength > 0){
                [mstring insertString:string atIndex:range.location];
            }
            else {
                //delete case - the length of replacement string is zero for a delete
                [mstring deleteCharactersInRange:range];
            }
        }

        //set the text now
        [textField setText:mstring];

        [mstring release];

        if (shouldMoveToNextField) {
            //
            //MOVE TO NEXT INPUT FIELD HERE
            //
        }
    }

    //always return no since we are manually changing the text field
    return NO;
}
9
votes

UPDATED CODE FOR SWIFT 3

@IBOutlet weak var tf1: UITextField!
@IBOutlet weak var tf2: UITextField!
@IBOutlet weak var tf3: UITextField!
@IBOutlet weak var tf4: UITextField!
override func viewDidLoad() {
    super.viewDidLoad()
    tf1.delegate = self
    tf2.delegate = self
    tf3.delegate = self
    tf4.delegate = self


    tf1.addTarget(self, action: #selector(self.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
    tf2.addTarget(self, action: #selector(self.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
    tf3.addTarget(self, action: #selector(self.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
    tf4.addTarget(self, action: #selector(self.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)

}
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(true)
    tf1.becomeFirstResponder()

}
func textFieldDidChange(textField: UITextField){

    let text = textField.text

    if text?.utf16.count==1{
        switch textField{
        case tf1:
            tf2.becomeFirstResponder()
        case tf2:
            tf3.becomeFirstResponder()
        case tf3:
            tf4.becomeFirstResponder()
        case tf4:
            tf4.resignFirstResponder()
        default:
            break
        }
    }else{

    }
}
8
votes

I know this is a very old question, but here's my approach for allowing a single numeric value only across four UITextFields, and automatically 'tabbing' to the next one (pin1-pin4 each represents a PIN number digit lol, and are retained as properties):

-(BOOL)textFieldShouldReturn:(UITextField*)textField;
{
    if (textField == pin1)
    {
        [pin2 becomeFirstResponder];
    }
    else if (textField == pin2)
    {
        [pin3 becomeFirstResponder];
    }
    else if (textField == pin3)
    {
        [pin4 becomeFirstResponder];
    }

    return NO;
}

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    // This allows numeric text only, but also backspace for deletes
    if (string.length > 0 && ![[NSScanner scannerWithString:string] scanInt:NULL])
        return NO;

    NSUInteger oldLength = [textField.text length];
    NSUInteger replacementLength = [string length];
    NSUInteger rangeLength = range.length;

    NSUInteger newLength = oldLength - rangeLength + replacementLength;

    // This 'tabs' to next field when entering digits
    if (newLength == 1) {
        if (textField == pin1)
        {
            [self performSelector:@selector(setNextResponder:) withObject:pin2 afterDelay:0.2];
        }
        else if (textField == pin2)
        {
            [self performSelector:@selector(setNextResponder:) withObject:pin3 afterDelay:0.2];
        }
        else if (textField == pin3)
        {
            [self performSelector:@selector(setNextResponder:) withObject:pin4 afterDelay:0.2];
        }
    }
    //this goes to previous field as you backspace through them, so you don't have to tap into them individually
    else if (oldLength > 0 && newLength == 0) {
        if (textField == pin4)
        {
            [self performSelector:@selector(setNextResponder:) withObject:pin3 afterDelay:0.1];
        }
        else if (textField == pin3)
        {
            [self performSelector:@selector(setNextResponder:) withObject:pin2 afterDelay:0.1];
        }
        else if (textField == pin2)
        {
            [self performSelector:@selector(setNextResponder:) withObject:pin1 afterDelay:0.1];
        }
    }

    return newLength <= 1;
}

- (void)setNextResponder:(UITextField *)nextResponder
{
    [nextResponder becomeFirstResponder];
}
5
votes

The following is for Swift 5 and handles textfields as an array instead of individual fields.

import UIKit

class MyViewController: UIViewController, UITextFieldDelegate {

@IBOutlet var digitFields: [UITextField]!

override func viewDidLoad() {
    super.viewDidLoad()

    digitFields.forEach {
        configureDigitField($0)
    }
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    digitFields[0].becomeFirstResponder()
}

fileprivate func configureDigitField(_ digitField: UITextField) {
    digitField.delegate = (self as UITextFieldDelegate)
    digitField.addTarget(self, action: #selector(self.textFieldDidChange(textField:)), for: UIControl.Event.editingChanged)
}

// Move to next field in digit fields if the value is populated
@objc fileprivate func textFieldDidChange(textField: UITextField) {
    if textField.text?.count == 1 {
        let remaining = digitFields.filter { $0.text?.count == 0 }
        if remaining.count > 0 {
            remaining[0].becomeFirstResponder()
        } else {
            digitFields.forEach { $0.resignFirstResponder() }
        }
    }
}

Results in:

enter image description here

This is dependent on the textfields being grouped in an array. This can be achieved in interface builder by configuring the collection of fields in the Outlet configuration screen:

Outlet configuration as a collection

which can be reached from the view controller properties on the last tab item enter image description here

Note that you need to manually add the

@IBOutlet var digitFields: [UITextField]!

to your view controller before you can add the text fields to it.

Summary of Code Behaviour

  • The view controller needs to be a UITextFieldDelegate to allow it to receive textfield events.
  • In the viewDidLoad function, each of the text fields in the array are initialised in the configureDigitField method
  • In the viewWillAppear method the first field in the array is readied to handle input (i.e. the first entry will take place in it)
  • The configureDigitalField function sets this view controller to receive events from the textfield (each of them as it is called for each textfield)
  • It also sets up a selector to call the textFieldDidChange function on result of a textfield edit changed event
  • textFieldDidChange method checks if the length of the text in the field is 1, and if so
  • checks for the remaining text fields where there is no value entered
  • takes the first remaining text field and sets it up to receive the next input
  • if no remaining fields are empty, resigns its position as first responder, so any more keypresses will not occur in any of the digit fields
3
votes

Swift 4.x

textField.addTarget(self, action: #selector(self.textFieldDidChange(textField:)), for: .editingChanged)

@objc func textFieldDidChange(textField: UITextField)
{
    let text = textField.text
    if text?.utf16.count == 1 {
        switch textField {
        case txtOtpNumber1:
            txtOtpNumber2.becomeFirstResponder()
        case txtOtpNumber2:
            txtOtpNumber3.becomeFirstResponder()
        case txtOtpNumber3:
            txtOtpNumber4.becomeFirstResponder()
        case txtOtpNumber4:
            txtOtpNumber4.resignFirstResponder()
        default:
            break
        }
    } else {
        switch textField {
        case txtOtpNumber4:
            txtOtpNumber3.becomeFirstResponder()
        case txtOtpNumber3:
            txtOtpNumber2.becomeFirstResponder()
        case txtOtpNumber2:
            txtOtpNumber1.becomeFirstResponder()
        case txtOtpNumber1:
            txtOtpNumber1.resignFirstResponder()
        default:
            break
        }
    }
}

PS. Chetan's answer updated for the current Swift.

1
votes

Although it is an old question, I just came cross it and I came with simpler solution. assuming we doing this for pass code so each box (UITextField) max length is one char.

- (BOOL) textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{
    if (![string isEqualToString:@""]) {
        textField.text = string;
        if ([textField isEqual:self.txtField1]) {
            [self.txtField2 becomeFirstResponder];
        }else if ([textField isEqual:self.txtField2]){
            [self.txtField3 becomeFirstResponder];
        }else if ([textField isEqual:self.txtField3]){
            [self.txtField4 becomeFirstResponder];
        }else{
            [textField resignFirstResponder];
        }
        return NO;
    }
    return YES;
}

-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
    if (textField.text.length > 0) {
        textField.text = @"";
    }
    return YES;
}