• Implement a customized pin code input control


    As a iOS engineer, you are asked to collect user’s input for a pin code. How can we make it?

    You can simply use a UITextField to do that, but it doesn’t make sense that user can enter any characters and super long text as a pin code input.

    We can use a customized UIView to reach this. First of all, we create a new file named PinView:

    @IBDesignable class PinView: UIView {  		// 1
        public var isIntact = false		// 2
        var pinCode: String? 					// 3
       @IBInspectable var maxDigits: Int = 4	// 4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Let’s break down it into several pieces:

    1. PinView extends UIView.
    2. var isIntact denote if the pin code is complete.
    3. var pinCode represent the pin code which is the user’s current input.
    4. Pin always means a length-limited, only-number-contained string. var maxDigits represent the maximum allowed length of the pin code. We decorate this vairable with a IBInspectable means we can see and modify its value in IB’s attribute inspector.

    Apart from that, we also need some IBInspectable variables to customize its appearance:

        @IBInspectable var digitWidth: CGFloat = 44				// 1
        @IBInspectable var digitHeight: CGFloat = 44			// 2
        @IBInspectable var gapBetweenDigits: CGFloat = 10	// 3
        @IBInspectable var digitBorderWidth: CGFloat = 2	// 4
        @IBInspectable var digitBorderColor: UIColor = .black 	// 5
        @IBInspectable var digitCornerRadius: CGFloat = 0	// 6
        @IBInspectable var textColor: UIColor = .black		// 7
        @IBInspectable var font: UIFont = UIFont.boldSystemFont(ofSize: 23) // 8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Let’s break it down step by step:

    1. Pin code input control should be made up of several seperated 0-9 characters.For each 0-9 number, we can use an UITextField to hold. For each TextField we can customize its size, i.e. width and:
    2. height.
    3. The gap between a text field and other one.
    4. Border width and:
    5. border color of each text field.
    6. Coner Radius of each text field.
    7. Text color.
    8. Font.

    Then we implement the initialization method:

        override public func awakeFromNib() {
            setupDigitFields()
        }
        override public func prepareForInterfaceBuilder() {
            setupDigitFields()
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    awakeFromNib will be called when PinView is loaded into storyboard if you are using storyboard to implement app’s UI.

    prepareForInterfaceBuilder will be invoked when IB is ready to update the storyboard’s canvas.

    setupDigitFields method is really responsible for class initialization:

    fileprivate func setupDigitFields() {
            // 1
            backgroundColor = .clear
            let stackView = UIStackView()
            stackView.axis = .horizontal
            stackView.translatesAutoresizingMaskIntoConstraints = false
            stackView.alignment = .leading
            stackView.spacing = gapBetweenDigits
            stackView.distribution = .fillProportionally
            addSubview(stackView)
            // 2
            NSLayoutConstraint.activate([
                stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
                stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
                stackView.widthAnchor.constraint(equalTo: widthAnchor),
                stackView.heightAnchor.constraint(equalTo: heightAnchor)
            ])
            // 3
            for tag in 1...maxDigits {
                let textField = UITextField()					// 4
                textField.tag = tag 					// 5
                // 6
                stackView.addArrangedSubview(textField)      
                NSLayoutConstraint.activate([
                    textField.widthAnchor.constraint(equalToConstant: digitWidth),
                    textField.heightAnchor.constraint(equalToConstant: digitHeight)
                ])
                setupDigitFieldStyle(textField) // 7
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    We will break down this snippet for you:

    1. Create a UIStackView to contain all text fields.
    2. Layout this stack view into super view correspondingly.
    3. Create a specific number of text fields base on maxDigits.
    4. All text field in a PinCode should be ordered so that we can move on back and forward. We give their tag a certain number by their orders.
    5. Add them into stack view one by one and constrain their size by digitWidth and digitHeight.
    6. Call the method to config their style.

    Then it’s the setupDigitFieldStyle function:

    fileprivate func setupDigitFieldStyle(_ textField: UITextField) {
            textField.delegate = self
            textField.backgroundColor = .white
            textField.keyboardType = .numberPad
            textField.textAlignment = .center
            textField.contentHorizontalAlignment = .center
            textField.layer.cornerRadius = digitCornerRadius
            textField.textColor = textColor
            textField.font = font
            textField.layer.borderWidth = digitBorderWidth
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    It’s no need to discuss the function, it’s all about UI styles. The most important line is :

    textField.delegate = self
    
    • 1

    So PinView need to adapt UITextFieldDelegate protocol:

    extension PinView: UITextFieldDelegate {
        // 1
        public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            var next = 0
            if string.isEmpty { //  2
                textField.deleteBackward()
                getPinCode()
                return false
            } else if string.count == 1 { // 3
                textField.text = string
                next = textField.tag + 1
                getPinCode()
            } else if string.count == maxDigits { // 4
                var mString = string
                for tag in 1...maxDigits {
                    guard let textfield = viewWithTag(tag) as? UITextField else { continue }
                    textfield.text = String(mString.removeFirst())
                }
                getPinCode()
            }
            if let nextDigitField = viewWithTag(next) as? UITextField { // 5
                nextDigitField.becomeFirstResponder()
            } else { // 6
                endEditing(true)
            }
            return false // 7
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    I’d like to break down these codes into :

    1. This method will be invoked when user is entering some characters into the text field.
    2. To check whether user is entering a backspace character, we can examine if the replacement string is empty. In swift 5.3, when the user deletes one or more characters, the replacement string is empty.If user is pressing the backspace key(‘del’ key on iOS keyboard), delete backward, figure out what the pin code actually is, and return false.
    3. Normally, the function should be invoked every time user put a character, but there is an exception: what can we do when user is pasting more than one character once for all? We need to identify this scenario. We check if replacement string is one character. If true, we replace text field’s content and add 1 to tag, so we can move forward the focus. In the same time, we figure out pin code.
    4. If replacement string contains more than one character, we can paste them into each text field in order. Likewise figure out pin code.
    5. Move to next text field.
    6. All text field is filled, the pin code is complete, end the editing.
    7. We always return false because we don’t wan to use UITextField’s default behavior.

    It’s necessary to define a delegate protocol to PinView so that we can get notice from outside when its state changes.

    public protocol PinViewDelegate {
        func didChangePinCode(_ pinView: PinView)
    }
    
    • 1
    • 2
    • 3

    didChangePinCode function has a parameter pinView, through this parameter, we can get all information about specific PinView instance.

    PinView also need a variable to refer to a PinViewDelegate:

        public var delegate: PinViewDelegate?
    
    • 1

    Next, it is the getPinCode function:

        public func getPinCode() {
            var pin = ""
            for tag in 1...maxDigits {
                guard let textfield = viewWithTag(tag) as? UITextField else { continue }
                pin += textfield.text!
            }
            pinCode = pin
            self.isIntact = pin.count >= maxDigits
            delegate?.didChangePinCode(self)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    As we mentioned before, PinView is made up of more than one text field, so we add up all text field’s text to a string that is where the pinCode comes in. We also compute isIntact correspondingly. At the end of the function, we call the delegate method to notify changes.

    Alright, It’s time to use our brand new PinView control.

    Open main.sotryboard, drag 2 UIViews in canvas, and set their class as PinView:

    Set their x,y and height constraints, but keep the width constraint empty because we want it self-adapting.

    Drag and drop a UIView into storyboard canvas:

    Config constraints for it.

    Create 3 IBOutlets for them:

    class ViewController: UIViewController {
        @IBOutlet weak var infoView: UIView!
        @IBOutlet weak var pinView2: PinView!
        @IBOutlet weak var pinView1: PinView!
    
    • 1
    • 2
    • 3
    • 4

    At the end of viewDidLoad, add lines:

    pinView1.delegate = self
    pinView2.delegate = self
    
    • 1
    • 2

    Extend ViewController to adapt PinViewDelegate:

    extension ViewController: PinViewDelegate {
        func didChangePinCode(_ pinView: PinView) {
            if pinView1.isIntact && pinView2.isIntact { // 1
                infoView.isHidden = false
                if pinView1.pinCode == pinView2.pinCode { // 2
                    infoView.backgroundColor = .green
                }else{
                    infoView.backgroundColor = .red
                }
            }else{
                infoView.isHidden = true
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Break down these lines into steps :

    1. if pin code and confirm pin code are both complete, we show the infoView.
    2. if pin code equals confirm pin code, we change the color ofinfoView to green , or else change it to red.
    3. if pin code and confirm pin code don’t complete together, we hide the infoView.

    Build & run, let’s give it a shot!

    As you see, once you enter a number in PinView, the focus will move on to next text field automatically. But it’s not that case if you enter a backspace! The focus - cursor position didn’t move back.

    Let’s fix it finally. Add a new swift file in project:

    import UIKit
    
    class PinDigitField: UITextField {
        
        override func deleteBackward() {
            if self.text?.count == 0 {
                moveToPrev()
            }else {
                self.text = nil
                moveToPrev()
            }
        }
        
        func moveToPrev() {
            if let prev = self.superview?.viewWithTag(self.tag - 1) as? UITextField {
                prev.becomeFirstResponder()
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    deleteBackward function comes from UITextInput protocol. We override this function to rephrase the behavior when backspace is presssed.

    moveToPrev function look up previous text field by tag index in PinView and move cursor position back to it.

    Back to PinView.swift, find setupDigitFields function, replace UITextField with PinDigitField:

    for tag in 1...maxDigits {
      let textField = PinDigitField() // UITextField()
    
    • 1
    • 2

    Build & run, give it a go!

  • 相关阅读:
    基于python技术的酒店管理系统
    mysql面试内容点
    计算机毕业设计Java即时高校信息发布系统(源码+系统+mysql数据库+lw文档)
    CSS 不规则的标题框样式
    EIP-3664合约研究笔记03--装备属性随机生成算法
    基于PHP+MySQL的企业员工培训管理系统
    学习记忆——宫殿篇——记忆宫殿——记忆桩——工人宿舍
    动手学深度学习——第五次学
    基于单片机的红外测距仪设计
    前端:nodejs版本管理神器nvm软件使用笔记
  • 原文地址:https://blog.csdn.net/kmyhy/article/details/126103685