Files
language4english/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift
T
2026-07-18 22:24:12 +08:00

178 lines
6.3 KiB
Swift

import UIKit
final class SpellingInputView: UIView, UITextFieldDelegate {
var onComplete: ((String) -> Void)?
private let hiddenField = UITextField()
private let stack = UIStackView()
private var chars: [String] = []
private var blankPositions: [Int] = []
private var filledBlanks: [String] = []
private var currentBlankIndex = 0
private var boxViews: [UIView] = []
private var cursorView: UIView?
private var inputEnabled = true
override init(frame: CGRect) {
super.init(frame: frame)
stack.axis = .horizontal
stack.spacing = 6
stack.alignment = .center
addSubview(stack)
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: topAnchor),
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
stack.centerXAnchor.constraint(equalTo: centerXAnchor),
stack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
])
hiddenField.delegate = self
hiddenField.autocorrectionType = .no
hiddenField.autocapitalizationType = .none
hiddenField.spellCheckingType = .no
hiddenField.keyboardType = .asciiCapable
hiddenField.frame = .zero
addSubview(hiddenField)
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reactivate)))
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
func configure(blankedWord: String, blankPositions: [Int]) {
chars = blankedWord.map { String($0) }
self.blankPositions = blankPositions
filledBlanks = Array(repeating: "", count: blankPositions.count)
currentBlankIndex = 0
inputEnabled = true
rebuildBoxes()
}
func activate() {
guard inputEnabled, !hiddenField.isFirstResponder else { return }
hiddenField.becomeFirstResponder()
}
func deactivate() {
inputEnabled = false
hiddenField.resignFirstResponder()
refreshBoxes()
}
@objc private func reactivate() {
activate()
}
// MARK: - UITextFieldDelegate
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard inputEnabled else { return false }
if string.isEmpty {
if currentBlankIndex > 0 {
currentBlankIndex -= 1
filledBlanks[currentBlankIndex] = ""
refreshBoxes()
}
} else if let char = string.last, char.isLetter, currentBlankIndex < blankPositions.count {
filledBlanks[currentBlankIndex] = String(char).lowercased()
currentBlankIndex += 1
refreshBoxes()
if currentBlankIndex == blankPositions.count {
var rebuilt = chars
for (i, pos) in blankPositions.enumerated() {
rebuilt[pos] = filledBlanks[i]
}
onComplete?(rebuilt.joined().lowercased())
}
}
textField.text = nil
return false
}
// MARK: - Boxes
private func rebuildBoxes() {
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
boxViews = chars.enumerated().map { index, ch in
let box = makeBox()
if ch != "_", let label = box.subviews.compactMap({ $0 as? UILabel }).first {
label.text = ch.uppercased()
label.isHidden = false
}
stack.addArrangedSubview(box)
NSLayoutConstraint.activate([
box.widthAnchor.constraint(equalToConstant: 36),
box.heightAnchor.constraint(equalToConstant: 44)
])
return box
}
refreshBoxes()
}
private func makeBox() -> UIView {
let box = UIView()
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
box.layer.cornerRadius = 8
box.layer.borderWidth = 1
box.layer.borderColor = UIColor.separator.cgColor
let label = UILabel()
label.font = .preferredFont(forTextStyle: .title2)
label.font = .systemFont(ofSize: label.font.pointSize, weight: .bold)
label.isHidden = true
label.tag = 100
box.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: box.centerXAnchor),
label.centerYAnchor.constraint(equalTo: box.centerYAnchor)
])
return box
}
private func refreshBoxes() {
cursorView?.removeFromSuperview()
cursorView = nil
for (index, box) in boxViews.enumerated() {
guard chars[index] == "_", let blankIndex = blankPositions.firstIndex(of: index) else {
continue
}
let label = box.viewWithTag(100) as? UILabel
let filled = filledBlanks[blankIndex]
label?.text = filled.uppercased()
label?.isHidden = filled.isEmpty
let isCurrent = inputEnabled && blankIndex == currentBlankIndex
box.layer.borderColor = isCurrent ? UIColor.tintColor.cgColor : UIColor.separator.cgColor
box.layer.borderWidth = isCurrent ? 2 : 1
if isCurrent, filled.isEmpty {
let cursor = UIView()
cursor.backgroundColor = .tintColor
box.addSubview(cursor)
cursor.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cursor.centerXAnchor.constraint(equalTo: box.centerXAnchor),
cursor.centerYAnchor.constraint(equalTo: box.centerYAnchor),
cursor.widthAnchor.constraint(equalToConstant: 2),
cursor.heightAnchor.constraint(equalToConstant: 22)
])
cursorView = cursor
startBlinking(cursor)
}
}
}
private func startBlinking(_ cursor: UIView) {
cursor.alpha = 1
UIView.animate(withDuration: 0.6, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction]) {
cursor.alpha = 0
}
}
}