CodeTextField
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 | |
} | |
} | |
} |
This comment has been minimized.
This comment has been minimized.
115行 blog 中是「<=」,代码中是「<」。测试下来「<」最后一位会显示不出来,应该 blog 中的是对的。 |
This comment has been minimized.
This comment has been minimized.
嗯,看了一下确实是,感谢指正。 |
This comment has been minimized.
This comment has been minimized.
有使用的demo |
This comment has been minimized.
This comment has been minimized.
怎么使用的? |
This comment has been minimized.
This comment has been minimized.
iOS 10上发现一个bug,开始输入验证码的时候textField的内容没有被隐藏,收起一次键盘后就好了 |
This comment has been minimized.
This comment has been minimized.
请问怎么用啊 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
最近正好要用到。之前实现了一版。感觉不太好,前来学习