UITextField, automatically move to next after 1 character

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

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];
}

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;
}

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{

    }
}