LNCaptchaInputView.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. //
  2. // LNCaptchaInputView.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/1/16.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. protocol LNCaptchaInputViewDelegate: NSObject {
  11. func onCaptchaInputChange(view: LNCaptchaInputView)
  12. }
  13. class LNCaptchaInputView: UIView {
  14. private let captchaCount: Int = 4
  15. private let stackView = UIStackView()
  16. private var inputViews: [UITextField] = []
  17. weak var delegate: LNCaptchaInputViewDelegate?
  18. var hasDone: Bool {
  19. inputViews.first { $0.text?.isEmpty != false } == nil
  20. }
  21. var curInput: String {
  22. inputViews.map { $0.text ?? "" }.joined()
  23. }
  24. override init(frame: CGRect) {
  25. super.init(frame: frame)
  26. setupViews()
  27. buildCaptchaInput()
  28. }
  29. required init?(coder: NSCoder) {
  30. fatalError("init(coder:) has not been implemented")
  31. }
  32. }
  33. extension LNCaptchaInputView: UITextFieldDelegate, LNTextFieldDelegate {
  34. func onDeleteBackward(_ textField: UITextField, oldText: String?) {
  35. if let oldText, !oldText.isEmpty { return }
  36. guard let index = inputViews.firstIndex(of: textField) else { return }
  37. if index == 0 { return }
  38. inputViews[index - 1].text = nil
  39. inputViews[index - 1].becomeFirstResponder()
  40. }
  41. func textFieldDidChangeSelection(_ textField: UITextField) {
  42. guard textField.text != nil else { return }
  43. let endPosition = textField.endOfDocument
  44. if textField.selectedTextRange?.start != endPosition {
  45. textField.selectedTextRange = textField.textRange(from: endPosition, to: endPosition)
  46. }
  47. }
  48. }
  49. extension LNCaptchaInputView {
  50. private func setupViews() {
  51. stackView.axis = .horizontal
  52. stackView.distribution = .equalCentering
  53. addSubview(stackView)
  54. stackView.snp.makeConstraints { make in
  55. make.edges.equalToSuperview()
  56. }
  57. }
  58. private func buildCaptchaInput() {
  59. let allViews = stackView.arrangedSubviews
  60. allViews.forEach {
  61. stackView.removeArrangedSubview($0)
  62. $0.removeFromSuperview()
  63. }
  64. inputViews.removeAll()
  65. for _ in 0..<captchaCount {
  66. let container = UIView()
  67. container.layer.cornerRadius = 10
  68. container.backgroundColor = .fill
  69. container.snp.makeConstraints { make in
  70. make.width.equalTo(64)
  71. }
  72. let input = LNTextField()
  73. input.delegate = self
  74. input.exDelegate = self
  75. input.font = .systemFont(ofSize: 32, weight: .semibold)
  76. input.textColor = .text_5
  77. input.keyboardType = .numberPad
  78. input.textAlignment = .center
  79. input.addAction(UIAction(handler: { [weak self, weak input] _ in
  80. guard let self, let input else { return }
  81. guard let text = input.text, !text.isEmpty else { return }
  82. input.text = "\(Int(text.prefix(1)) ?? 0)"
  83. guard let index = inputViews.firstIndex(of: input) else { return }
  84. if index < inputViews.count - 1 {
  85. inputViews[index + 1].text = nil
  86. inputViews[index + 1].becomeFirstResponder()
  87. } else {
  88. input.resignFirstResponder()
  89. }
  90. delegate?.onCaptchaInputChange(view: self)
  91. }), for: .editingChanged)
  92. container.addSubview(input)
  93. input.snp.makeConstraints { make in
  94. make.horizontalEdges.equalToSuperview()
  95. make.centerY.equalToSuperview()
  96. }
  97. stackView.addArrangedSubview(container)
  98. inputViews.append(input)
  99. }
  100. inputViews.first?.becomeFirstResponder()
  101. }
  102. }
  103. private protocol LNTextFieldDelegate: NSObject {
  104. func onDeleteBackward(_ textField: UITextField, oldText: String?)
  105. }
  106. private class LNTextField: UITextField {
  107. weak var exDelegate: LNTextFieldDelegate?
  108. override func deleteBackward() {
  109. let oldText = text
  110. super.deleteBackward()
  111. exDelegate?.onDeleteBackward(self, oldText: oldText)
  112. }
  113. }