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:
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:
which can be reached from the view controller properties on the last tab item
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{
}
}