用起来像 UITextField 一样的自定义验证码输入框

2,149 阅读2分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

在 App 的注册、登陆流程中,经常会有验证手机验证码的流程。为了方便、清晰的显示用户输入的验证码,设计师会对用户输入的过程进行特殊的设计。

例如下面是「得到」APP 的手机验证码的输入 UI。

IMG_6011.png

如果定制验证码的输入框呢?

在接收用户输入内容时,除了常用的是 UITextFieldUITextView,我们还可以自定义一个输入框。文字处理的控件需要实现 UITextInput 的协议,UITextField 和 UITextView 都实现了 UITextInput。当我们实现了 UITextInput 协议,再绘制需要的 UI 元素,一个自定义手机验证码输入框也就实现了。

UITextInput 协议

先看 UITextInput 的定义:

public protocol UITextInput : UIKeyInput {
    // 省略具体内容
}

public protocol UIKeyInput : UITextInputTraits {
    // 省略具体内容
}

public protocol UITextInputTraits : NSObjectProtocol {
    // 省略具体内容
}

通过 UITextInput 协议,可以获取到用户编辑的过程,比如:

  • insertText(_:):插入字符
  • deleteBackward():删除字符
  • replace(_:withText:) 替换文本

当用户输入的内容发生变化时,调用 setNeedsDisplay() 方法通知需要重绘 UI。

自定义 UI

自定义 UI 可以通过重写(override)draw(_:) 方法中来实现。在 draw(_:) 方法中,可以绘制边框,光标和字符等 UI 元素。

比如,我们要实现一个名为 UnitField 的自定义验证码输入框,定义如下:

open class UnitField: UIControl {
}

draw(_:) 方法中绘制边框,字符等 UI 元素:

open override func draw(_ rect: CGRect) {
    // 计算每个验证码大小
    let width = (rect.size.width + CGFloat(unitSpace)) / CGFloat(inputUnitCount) - unitSpace
    let height = rect.size.height
    let unitSize = CGSize(width: width, height: height)

    // 获取上下文信息
    mCtx = UIGraphicsGetCurrentContext();

    // 绘制填充
    fill(rect: rect, unitSize: unitSize)
    // 绘制边框
    drawBorder(rect: rect, unitSize: unitSize)
    // 绘制文字
    drawText(rect: rect, unitSize: unitSize)
    // 绘制跟踪框
    drawTrackBorder(rect: rect, unitSize: unitSize)
}

其中,绘制字符时,首先计算出每个字符的位置,然后调用 NSString 的 draw(in:withAttributes:) 方法进行绘制。具体代码为:

func drawText(rect: CGRect, unitSize: CGSize) {
    guard hasText else {
        return
    }
    
    let attr = [NSAttributedString.Key.foregroundColor: textColor,
                NSAttributedString.Key.font: textFont]

    for i in 0 ..< characters.count {
        let unitRect = CGRect(x: CGFloat(i) * (unitSize.width + unitSpace), y: 0, width: unitSize.width, height: unitSize.height)
        let yOffset = style == .border ? 0 : borderWidth

        let subString = NSString(string: String(characters[i]))
        let oneTextSize = subString.size(withAttributes: attr)
        var drawRect = unitRect.insetBy(dx: (unitRect.size.width - oneTextSize.width) / 2,
                                        dy: (unitRect.size.height - oneTextSize.height) / 2)

        drawRect.size.height -= yOffset
        subString.draw(in: drawRect, withAttributes: attr)
    }
}

另外,iOS 12 之后开始支持验证码自动填充功能。UITextInputTraits 协议中定义了 textContentType: UITextContentType 属性,将其设置为 oneTimeCode 就支持了自动填充功能。

完整的项目

如果想要深入了解,可以查看 UnitField 的完整项目代码,一个用起来像 UITextField 一样的自定义验证码输入框。

目前已支持 cocopose 使用,并支持 RxSwift。