Codementor Events

OTP Code View

Published Jan 06, 2019

Two years ago, I did a view for this purpose. Detail is on my github: https://github.com/nguyentruongky/ActiveCode.

It’s 100% code, without auto layout. Today, I make another view, with same purpose, but use auto layout, better UI and much easier to maintain.

UI will be like this.

You can try it yourself before follow this tutorial. Find your solution, that can be much better than this.

The solution idea:

  • Add number of labels to display input letters.
  • Add a textfield to show the keyboard, but set it be overlapped by other view (or out of frame)
  • Capture every character input and update the labels.

Let's do it.

Initilize view

Init view with number of digits you need and an action to validate your OTP from your controller. The number of digits should be less than 6 for good UI.

private var digitCount = 0
private var validate: ((String) -> Void)?
convenience init(digitCount: Int, validate: @escaping ((String) -> Void)) {
    self.init(frame: CGRect.zero)
    self.digitCount = digitCount
    self.validate = validate
    setupView()
}
  • Init the view with number of digits and a callback to validate the code input.

Setup UI

override func setupView() {
  // (1)
  guard digitCount > 0 else { return }

  // (2)
  var constraints = "H:|-8-"
  for i in 0 ..< digitCount {
      let label = makeLabel()
      if i > 0 {
          label.width(toView: labels[0])
      }
      constraints += "[v\(i)]-8-"
  }
  constraints += "|"
  addConstraints(withFormat: constraints, arrayOf: labels)
  height(60)

  // (3)
  setCode(at: 0, active: true)
  hiddenTextField.becomeFirstResponder()
  addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(becomeFirstResponder)))
}

private func makeLabel() -> UILabel {
  let label = knUIMaker.makeLabel(font: UIFont.systemFont(ofSize: 45),
                              color: color_69_125_245,
                              alignment: .center)
  // (4)
  label.createRoundCorner(5)
  label.createBorder(0.5, color: color_102)

  // (5)
  addSubview(label)
  label.vertical(toView: self)
  labels.append(label)
  return label
}

(1)

  • Prevent code running while no digit. Only support init view by code with init(digitCount:validate), so should prevent other init way to make wrong behaviour.

(2)

  • Set Auto layout code. Create a Visual Format Language string (like this: "H:|-8-[v0]-[v1]-8-|"). Detail is here.

  • To make sure all digit indicators have same width, I set widthConstraint equal to the first indicator.

(3)

  • Minor stuffs, make first indicator active, set focus to the hiddenTextField to show keyboard.

(4)

  • This is how the indicator look. Want to change UI, just do it here.

(5)

  • This is the label to display the indicator.

Set hidden UITextField

Add a UITextField, which is overlapped to use its keyboard and delegate. Every key input, I will update the digit indicator by catch the character in UITextFieldDelegate

lazy var hiddenTextField = addHiddenTextField()
private func addHiddenTextField() -> UITextField {
    let tf = UITextField()
    tf.translatesAutoresizingMaskIntoConstraints = false
    tf.keyboardType = .numberPad
    tf.isHidden = true
    tf.delegate = self

    addSubviews(views: tf)
    tf.fill(toView: self)

    return tf
}
override func becomeFirstResponder() -> Bool {
    hiddenTextField.becomeFirstResponder()
    return true
}

Conform UITextFieldDelegate and update method textfield(shouldChangeCharactersIn:replacementString). This is the most important thing in this class.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    var newText = string
    // (1)
    if isInvalid {
        isInvalid = false
    } else {
        newText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
    }

    // (2)
    let codeLength = newText.length
    guard codeLength <= digitCount else { return false }
    textField.text = newText

    // (3)
    func setTextToActiveBox() {
        for i in 0 ..< codeLength {
            let char = textField.text!.substring(from: i, to: i)
            labels[i].text = char
            setCode(at: i, active: true)
        }
    }

    // (4)
    func setTextToInactiveBox() {
        for i in codeLength ..< digitCount {
            labels[i].text = ""
            setCode(at: i, active: false)
        }

        if codeLength <= digitCount - 1 {
            setCode(at: codeLength, active: true)
        }
    }

    setTextToActiveBox()
    setTextToInactiveBox()

    if codeLength == digitCount {
        validateCode(code: textField.text!)
    }
    return false
}

(1)

  • Reset all boxes when the code is invalid and type new character.

(2)(3)(4)

  • Update every character from hiddenTextField to every indicator box. Rest of boxes will be set to empty.

Result

The main part is ready. Minors methods, properties are in the lib in github. Link is https://github.com/nguyentruongky/knOtpView.

Discover and read more posts from Ky Nguyen
get started
post commentsBe the first to share your opinion
Show more replies