Last active
December 11, 2023 07:19
-
-
Save kemchenj/bc51eb610059c49a26d08bdc73d4743b to your computer and use it in GitHub Desktop.
CodeTextField
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class CodeTextField: UITextField, UITextFieldDelegate { | |
let codeLength: Int | |
var characterSize: CGSize | |
var characterSpacing: CGFloat | |
let textPreprocess: (String) -> String | |
let validCharacterSet: CharacterSet | |
let characterLabels: [CharacterLabel] | |
override var textColor: UIColor? { | |
get { return characterLabels.first?.textColor } | |
set { characterLabels.forEach { $0.textColor = newValue } } | |
} | |
override var delegate: UITextFieldDelegate? { | |
get { return super.delegate } | |
set { assertionFailure() } | |
} | |
init( | |
codeLength: Int, | |
characterSize: CGSize, | |
characterSpacing: CGFloat, | |
validCharacterSet: CharacterSet, | |
characterLabelGenerator: () -> CharacterLabel, | |
textPreprocess: @escaping (String) -> String = { $0 } | |
) { | |
self.codeLength = codeLength | |
self.characterSize = characterSize | |
self.characterSpacing = characterSpacing | |
self.validCharacterSet = validCharacterSet | |
self.textPreprocess = textPreprocess | |
self.characterLabels = (0..<codeLength).map { _ in characterLabelGenerator() } | |
super.init(frame: .zero) | |
loadSubviews() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override var intrinsicContentSize: CGSize { | |
return CGSize( | |
width: characterSize.width * CGFloat(codeLength) + characterSpacing * CGFloat(codeLength - 1), | |
height: characterSize.height | |
) | |
} | |
private func loadSubviews() { | |
super.textColor = UIColor.clear | |
clipsToBounds = true | |
super.delegate = self | |
addTarget(self, action: #selector(updateLabels), for: .editingChanged) | |
clearsOnBeginEditing = false | |
clearsOnInsertion = false | |
characterLabels.forEach { | |
$0.textAlignment = .center | |
addSubview($0) | |
} | |
} | |
override func caretRect(for position: UITextPosition) -> CGRect { | |
let currentEditingPosition = text?.count ?? 0 | |
let superRect = super.caretRect(for: position) | |
guard currentEditingPosition < codeLength else { | |
return CGRect(origin: .zero, size: .zero) | |
} | |
let x = (characterSize.width + characterSpacing) * CGFloat(currentEditingPosition) + characterSize.width / 2 - superRect.width / 2 | |
return CGRect( | |
x: x, | |
y: superRect.minY, | |
width: superRect.width, | |
height: superRect.height | |
) | |
} | |
override func textRect(forBounds bounds: CGRect) -> CGRect { | |
let origin = super.textRect(forBounds: bounds) | |
return CGRect( | |
x: -bounds.width, | |
y: 0, | |
width: 0, | |
height: origin.height | |
) | |
} | |
override func placeholderRect(forBounds bounds: CGRect) -> CGRect { | |
return .zero | |
} | |
override func borderRect(forBounds bounds: CGRect) -> CGRect { | |
return .zero | |
} | |
override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { | |
return [] | |
} | |
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | |
let newText = text | |
.map { $0 as NSString } | |
.map { $0.replacingCharacters(in: range, with: string) } | |
.map(textPreprocess) ?? "" | |
let newTextCharacterSet = CharacterSet(charactersIn: newText) | |
let isValidLength = newText.count <= codeLength | |
let isUsingValidCharacterSet = validCharacterSet.isSuperset(of: newTextCharacterSet) | |
if isValidLength, isUsingValidCharacterSet { | |
textField.text = newText | |
sendActions(for: .editingChanged) | |
} | |
return false | |
} | |
override func deleteBackward() { | |
super.deleteBackward() | |
sendActions(for: .editingChanged) | |
} | |
@objc func updateLabels() { | |
let text = self.text ?? "" | |
var chars = text.map { Optional.some($0) } | |
while chars.count < codeLength { | |
chars.append(nil) | |
} | |
zip(chars, characterLabels).enumerated().forEach { args in | |
let (index, (char, charLabel)) = args | |
charLabel.update( | |
character: char, | |
isFocusingCharacter: index == text.count || (index == text.count - 1 && index == codeLength - 1), | |
isEditing: isEditing | |
) | |
} | |
} | |
override func becomeFirstResponder() -> Bool { | |
defer { updateLabels() } | |
return super.becomeFirstResponder() | |
} | |
override func resignFirstResponder() -> Bool { | |
defer { updateLabels() } | |
return super.resignFirstResponder() | |
} | |
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { | |
let paste = #selector(paste(_:)) | |
return action == paste | |
} | |
// 任何调整选择范围的行为都会直接把 insert point 调到最后一次 | |
override var selectedTextRange: UITextRange? { | |
get { return super.selectedTextRange } | |
set { super.selectedTextRange = textRange(from: endOfDocument, to: endOfDocument) } | |
} | |
override func paste(_ sender: Any?) { | |
super.paste(sender) | |
updateLabels() | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
characterLabels.enumerated().forEach { args in | |
let (index, label) = args | |
label.frame = CGRect( | |
x: (characterSize.width + characterSpacing) * CGFloat(index), | |
y: 0, | |
width: characterSize.width, | |
height: characterSize.height | |
) | |
} | |
} | |
class CharacterLabel: UILabel { | |
var isEditing = false | |
var isFocusingCharacter = false | |
func update(character: Character?, isFocusingCharacter: Bool, isEditing: Bool) { | |
self.text = character.map { String($0) } | |
self.isEditing = isEditing | |
self.isFocusingCharacter = isFocusingCharacter | |
} | |
} | |
} |
115行 blog 中是「<=」,代码中是「<」。测试下来「<」最后一位会显示不出来,应该 blog 中的是对的。
115行 blog 中是「<=」,代码中是「<」。测试下来「<」最后一位会显示不出来,应该 blog 中的是对的。
嗯,看了一下确实是,感谢指正。
有使用的demo
怎么使用的?
iOS 10上发现一个bug,开始输入验证码的时候textField的内容没有被隐藏,收起一次键盘后就好了
请问怎么用啊
如何监听输入完了
有注释或者使用实例就完美了
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
最近正好要用到。之前实现了一版。感觉不太好,前来学习