引言
在实际开发过程中验证码输入框是一个很常见UI界面。通常来讲有简单的输入框,也有方格的输入框,其中相对较为棘手就是这种方格输入框里面还需要显示光标的情况。本篇博客我们就来主要讨论一下方格带光标的验证码输入框样式。
实现方案
在着手实现这个功能之前,首先我们需要明确需要做的事情:
- 创建出方格,并且每个方格都需要响应用户输入。
- 在方格输入一个数字后,需要自动切换到下一个方格响应输入。
- 在方格删除一个数字后,需要自动切换到上一个方格响应。
- 所有方格都输入完成之后,需要通知页面控制器。
1.创建方格
既然每个方格都需要响应用户的输入(且显示光标),那我们很容易就会想到方格需要有UITextFiled用来响应用户输入和光标。当切换到下一个方格时,上一个方格还需要显示已经输入的内容,这里我们采用一个UILabel来实现。
这样我们自定义一个单元输入框大概结构就有了,代码如下:
class PHInputItemView: UIView, UITextFieldDelegate {
/// 显示label
private let label = UILabel()
/// 实际输入框
private let codeField = PHCodeField()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setLayout()
setEvent()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
self.layer.masksToBounds = true
self.layer.cornerRadius = 8.0
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.clear.cgColor
self.addSubview(codeField)
codeField.font = UIFont.systemFont(ofSize: 30, weight: .medium)
codeField.textAlignment = .center
codeField.textColor = .black
codeField.tintColor = .black
codeField.backgroundColor = .clear
self.addSubview(label)
label.font = UIFont.systemFont(ofSize: 30, weight: .medium)
label.textAlignment = .center
label.textColor = .black
label.backgroundColor = .black.withAlphaComponent(0.4)
}
func setLayout() {
label.frame = self.bounds
codeField.frame = self.bounds
}
func setEvent() {
}
override func layoutSubviews() {
super.layoutSubviews()
setLayout()
}
}
由于我们需要在用户输入或者删除时进行一些切换的操作,所以这个自定义的单元输入需要将事件回调出去,这里我们采用了闭包的形式进行回调,操作如下:
用户输入
增加输入回调闭包:
/// 输入回调
var inputAction: ((String) -> Void)?
设置代理:
codeField.delegate = self
实现代理方法:
//MARK: UITextFieldDelegate
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.count == 1 {
self.label.text = string
self.inputAction?(string)
return false
}
return true
}
用户删除
用户删除,我们使用代理方法或者是监听通知时,如果输入框内本来就没有内容是没有获取到回调的,因此我们采用了重写UITextFiled的方式来获取删除按钮的点击事件。
代码如下:
import UIKit
class PHCodeField: UITextField {
/// 键盘删除按钮点击回调
var deleteAction: (() -> Void)?
override func deleteBackward() {
super.deleteBackward()
deleteAction?()
}
}
增加删除回调:
/// 删除回调
var deleteAction: (() -> Void)?
调用删除回调:
func setEvent() {
codeField.deleteAction = { [weak self] in
guard let self = self else { return }
self.deleteAction?()
}
}
自定义单元输入框状态
另外为了区分输入框是否处于响应状态我们采用不同的UI样式来进行区分一下,代码如下:
/// 选中属性
var isSelected:Bool = false {
didSet {
if isSelected {
self.layer.borderColor = UIColor.orange.cgColor
codeField.becomeFirstResponder()
codeField.isHidden = false
label.isHidden = true
} else {
self.layer.borderColor = UIColor.clear.cgColor
codeField.resignFirstResponder()
codeField.isHidden = true
label.isHidden = false
}
}
}
获取验证码
添加一个只读属性,读取验证码的值。
/// 验证码
var code:String? {
get {
return label.text
}
}
2.创建验证码输入视图
这个相对简单一些,我们只需要根据验证码的个数将刚刚创建的最小单元输入口整齐的排在上面就可以了,案例中我们设置输入框的个数为5,间距为8,size为{40,54}。
class PHVerifyCodeView: UIView {
/// 验证码数字个数
private var count = 5
/// 默认间距
private var space:CGFloat = 8.0
/// stackView
private let contentView = UIView()
/// 默认size
private var itemSize:CGSize = CGSize(width: 40.0, height: 54.0)
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setLayout()
addInputItemViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
self.addSubview(contentView)
}
func setLayout() {
contentView.frame = self.bounds
}
func addInputItemViews() {
var offsetX = (contentView.bounds.size.width - CGFloat(count) * itemSize.width - CGFloat(count - 1) * space) / 2.0
for i in 0 ..< count {
let itemView = PHInputItemView(frame: CGRect(x: offsetX, y: 0.0, width: itemSize.width, height: itemSize.height))
contentView.addSubview(itemView)
if i == 0 {
itemView.isSelected = true
}
offsetX += itemSize.width + space
}
}
override func layoutSubviews() {
super.layoutSubviews()
setLayout()
}
}
3.响应用户输入,自动切换到下一个单元输入框
响应用户的输入回调,代码如下:
itemView.inputAction = { [weak self] text in
guard let self = self else { return }
self.input(text: text, index: i, itemView: itemView)
}
实现自动切换到下一个输入框,这里面我们需要注意到临界值:
private func input(text:String,index:Int,itemView:PHInputItemView) {
if index == count - 1 {
itemView.isSelected = false
} else {
let nextItemView = contentView.subviews[index + 1] as! PHInputItemView
nextItemView.isSelected = true
itemView.isSelected = false
}
}
4.相应用户删除,自动切换到上一个输入框
响应用户删除,代码如下:
itemView.deleteAction = { [weak self] in
guard let self = self else { return }
self.delete(index: i, itemView: itemView)
}
实现自动切换到上一个输入框,代码如下:
private func delete(index:Int,itemView:PHInputItemView) {
if index == 0 {
return
}
let preItemView = contentView.subviews[index - 1] as! PHInputItemView
preItemView.isSelected = true
itemView.isSelected = false
}
5.输入完成回调
添加输入完成回调:
/// 输入完成回调
var inputFinish: ((String) -> Void)?
在用户输入完成之后,调用回调函数传入验证码:
private func input(text:String,index:Int,itemView:PHInputItemView) {
if index == count - 1 {
itemView.isSelected = false
inputFinish?(getVerifyCode())
} else {
let nextItemView = contentView.subviews[index + 1] as! PHInputItemView
nextItemView.isSelected = true
itemView.isSelected = false
}
}
/// 获取验证码
func getVerifyCode() -> String {
var code = ""
for view in contentView.subviews {
let itemView = view as! PHInputItemView
code += itemView.code ?? ""
}
return code
}
6.使用验证码输入框
在视图控制器中直接使用,代码如下:
let verifyCodeView = PHVerifyCodeView(frame: CGRect(x: 0.0, y: 100.0, width: self.view.bounds.size.width, height: 54.0))
verifyCodeView.backgroundColor = .cyan
self.view.addSubview(verifyCodeView)
实现输入完成的回调:
verifyCodeView.inputFinish = { code in
print("输入完成:\(code)")
}
结语
实现一个带有光标的方块输入框就完成了,如果不需要显示光标,那么实现起来会非常简单。我们只需创建几个 UILabel
和一个隐藏的 UITextField
,然后监听用户输入,将所有内容分割并按顺序显示到 UILabel
中即可。
每个需求和设计的实现方案都不是唯一的,如果大家有更好的方案,欢迎分享出来。