Issue #347

Add a hidden UITextField to view hierarchy, and add UITapGestureRecognizer to activate that textField.

Use padding string with limit to the number of labels, and prefix to get exactly n characters.

code

DigitView.swift

import UIKit

final class DigitView: UIView {
    lazy var stackView: UIStackView = {
        let view = UIStackView()
        view.axis = .horizontal
        view.distribution = .equalSpacing
        return view
    }()

    private(set) var boxes: [UIView] = []
    private(set) var labels: [UILabel] = []

    lazy var hiddenTextField: UITextField = {
        let textField = UITextField()
        textField.alpha = 0
        textField.keyboardType = .numbersAndPunctuation
        return textField
    }()

    lazy var tapGR = UITapGestureRecognizer(target: self, action: #selector(handle(_:)))

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()

        addGestureRecognizer(tapGR)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        boxes.forEach {
            $0.layer.borderWidth = 1
            $0.layer.borderColor = R.color.primary.cgColor
            $0.layoutIfNeeded()
            $0.layer.cornerRadius = $0.bounds.height / 2
        }
    }

    @objc private func handle(_ tapGR: UITapGestureRecognizer) {
        hiddenTextField.becomeFirstResponder()
    }

    private func setup() {
        addSubviews([hiddenTextField, stackView])
        boxes = Array(0..<6).map { _ in
            return UIView()
        }

        labels = boxes.map { box in
            let label = UILabel()
            label.font = R.customFont.semibold(16)
            label.textAlignment = .center
            label.textColor = R.color.primary
            box.addSubview(label)

            NSLayoutConstraint.on([
                label.centerXAnchor.constraint(equalTo: box.centerXAnchor),
                label.centerYAnchor.constraint(equalTo: box.centerYAnchor)
            ])

            return label
        }

        boxes.forEach {
            stackView.addArrangedSubview($0)

            NSLayoutConstraint.on([
                $0.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.9),
                $0.widthAnchor.constraint(equalTo: $0.heightAnchor, multiplier: 1.0)
            ])
        }

        NSLayoutConstraint.on([
            stackView.pinEdges(view: self, inset: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: -16))
        ])
    }
}

DigitHandler.swift

final class DigitHandler: NSObject {
    let digitView: DigitView

    init(digitView: DigitView) {
        self.digitView = digitView
        super.init()

        digitView.hiddenTextField.delegate = self
        digitView.hiddenTextField.addTarget(self, action: #selector(handle(_:)), for: .editingChanged)
    }

    @objc private func handle(_ textField: UITextField) {
        guard let text = textField.text else {
            return
        }

        let count = digitView.labels.count
        let paddedText = String(text.padding(toLength: count, withPad: "-", startingAt: 0).prefix(count))
        zip(digitView.labels, paddedText).forEach { tuple in
            tuple.0.text = String(tuple.1)
        }
    }
}

extension DigitHandler: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let text = textField.text ?? ""
        return text.count < digitView.labels.count
    }
}