2
\$\begingroup\$

I am working on an app in which I have to pass 6 digits OTP through 6 textFields in which you to provide only one character and after that it automatically goes to another textField. I created 6 textFields outlets and used this code.

override func viewDidLoad() {
    super.viewDidLoad()

txtOTP1.delegate = self
txtOTP2.delegate = self
txtOTP3.delegate = self
txtOTP4.delegate = self
txtOTP5.delegate = self
txtOTP6.delegate = self

}

And I used this logic to create the functionality

 func textField(_ textField: UITextField, shouldChangeCharactersIn range:NSRange, replacementString string: String) -> Bool {
    // Range.length == 1 means,clicking backspace
    if (range.length == 0){
        if textField == txtOTP1 {
            txtOTP2?.becomeFirstResponder()
        }
        if textField == txtOTP2 {
            txtOTP3?.becomeFirstResponder()
        }
        if textField == txtOTP3 {
            txtOTP4?.becomeFirstResponder()
        }
        if textField == txtOTP4 {
            txtOTP5?.becomeFirstResponder()
        }
        if textField == txtOTP5 {
            txtOTP6?.becomeFirstResponder()
        }
        if textField == txtOTP6 {
            txtOTP6?.resignFirstResponder()
        }
        textField.text? = string
        return false
    }else if (range.length == 1) {
        if textField == txtOTP6 {
            txtOTP5?.becomeFirstResponder()
        }
        if textField == txtOTP5 {
            txtOTP4?.becomeFirstResponder()
        }
        if textField == txtOTP4 {
            txtOTP3?.becomeFirstResponder()
        }
        if textField == txtOTP3 {
            txtOTP2?.becomeFirstResponder()
        }
        if textField == txtOTP2 {
            txtOTP1?.becomeFirstResponder()
        }
        if textField == txtOTP1 {
            txtOTP1?.resignFirstResponder()
        }
        textField.text? = ""
        return false
    }
    return true
}

But it is very long and messy. Is there anyway I can make it simple?

\$\endgroup\$
  • 1
    \$\begingroup\$ An array of text fields looks promising. I am not a swift person, so no review. \$\endgroup\$ – vnp Sep 18 '18 at 20:08
  • \$\begingroup\$ You can use ib oulet collection and set the tag on the text input to see which textinput is it. But i think selecting the other textbox only with comparing the tag is also longer than your code. \$\endgroup\$ – muescha Sep 19 '18 at 8:29
  • \$\begingroup\$ yes that is also a long process by using tags and compare \$\endgroup\$ – Wings Sep 19 '18 at 8:32
3
\$\begingroup\$

Your solution to this particular problem is fine in its own way. However when it comes to scaling, it will become quite messy.

I don't see any scope of improvement/tweaking in your approach, so I suggest an alternative one.

As pointed out in the comments, you can get what you want with Outlet Collections, since they are basically an array of UIKit objects, thus making it easy to manipulate them for our specific case.


First off, connect your textfields to an Outlet Collection, and delegates in IB itself, which gets rid of the redundant @IBOutlet and textField.delegate = self statements.

@IBOutlet var textFieldsOutletCollection: [UITextField]!

Outlet Collection -- IB

We are going to use a dictionary with the textfield as the key and its corresponding index in the outlet collection as the value:

var textFieldsIndexes:[UITextField:Int] = [:]

override func viewDidLoad() {
    for index in 0 ..< textFieldsOutletCollection.count {
        textFieldsIndexes[textFieldsOutletCollection[index]] = index
    }
    super.viewDidLoad()
}

The basic idea is, we can get the index of the textfield in our collection by supplying (to the dictionary) the textfield object we get in shouldChangeCharactersIn method, and then make the next or previous textfield the first responder:

enum Direction { case left, right }

func setNextResponder(_ index:Int?, direction:Direction) {

    guard let index = index else { return }

    if direction == .left {
        index == 0 ?
            (_ = textFieldsOutletCollection.first?.resignFirstResponder()) :
            (_ = textFieldsOutletCollection[(index - 1)].becomeFirstResponder())
    } else {
        index == textFieldsOutletCollection.count - 1 ?
            (_ = textFieldsOutletCollection.last?.resignFirstResponder()) :
            (_ = textFieldsOutletCollection[(index + 1)].becomeFirstResponder())
    }

}

Now the shouldChangeCharactersIn method becomes more simple and clean:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if range.length == 0 {
        setNextResponder(textFieldsIndexes[textField], direction: .right)
        textField.text = string            
        return true
    } else if range.length == 1 {
        setNextResponder(textFieldsIndexes[textField], direction: .left)
        textField.text = ""            
        return false
    }
    return false
}

With all the above changes, your viewcontroller should look like this:

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var textFieldsOutletCollection: [UITextField]!

    var textFieldsIndexes:[UITextField:Int] = [:]

    override func viewDidLoad() {
        super.viewDidLoad()
        for index in 0 ..< textFieldsOutletCollection.count {
            textFieldsIndexes[textFieldsOutletCollection[index]] = index
        }
    }

    enum Direction { case left, right }

    func setNextResponder(_ index:Int?, direction:Direction) {

        guard let index = index else { return }

        if direction == .left {
            index == 0 ?
                (_ = textFieldsOutletCollection.first?.resignFirstResponder()) :
                (_ = textFieldsOutletCollection[(index - 1)].becomeFirstResponder())
        } else {
            index == textFieldsOutletCollection.count - 1 ?
                (_ = textFieldsOutletCollection.last?.resignFirstResponder()) :
                (_ = textFieldsOutletCollection[(index + 1)].becomeFirstResponder())
        }

    }

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

        if range.length == 0 {
            textField.text = string
            setNextResponder(textFieldsIndexes[textField], direction: .right)
            return true
        } else if range.length == 1 {
            textField.text = ""
            setNextResponder(textFieldsIndexes[textField], direction: .left)
            return false
        }
        return false

    }

}
\$\endgroup\$
  • \$\begingroup\$ I tried your code but it is not working infact it can't go to the next textfield. See my above image \$\endgroup\$ – Wings Sep 24 '18 at 9:42
  • \$\begingroup\$ @V_rohit Did you properly connect all the text fields to the single outlet collection? It should work. And also make sure the delegates are set. \$\endgroup\$ – Badhan Ganesh Sep 24 '18 at 9:48
  • \$\begingroup\$ yes I connected all the outlets to the textfield array \$\endgroup\$ – Wings Sep 24 '18 at 9:50
  • \$\begingroup\$ delegates are set ?? do I need to create separate outlets for textfield \$\endgroup\$ – Wings Sep 24 '18 at 9:53
  • \$\begingroup\$ I’ll edit my answer to be more clear. \$\endgroup\$ – Badhan Ganesh Sep 24 '18 at 9:54
0
\$\begingroup\$

I would recommend using a single text box have the same background and then have letter spacing. This would give u less amount of code and better visibility.

\$\endgroup\$
  • \$\begingroup\$ Your answer does not review or improve the OP's implementation. Please be elaborate with your answer. \$\endgroup\$ – Badhan Ganesh Oct 3 '18 at 7:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.