拼写改为确认按钮手动提交,点击已填字母可删除重输

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:42:17 +08:00
parent 9893d5408a
commit 71dfa297ed
2 changed files with 61 additions and 22 deletions
@@ -89,7 +89,7 @@ final class QuizViewController: UIViewController {
var nextConfig = UIButton.Configuration.borderedProminent() var nextConfig = UIButton.Configuration.borderedProminent()
nextConfig.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16) nextConfig.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16)
nextButton.configuration = nextConfig nextButton.configuration = nextConfig
nextButton.addAction(UIAction { [weak self] _ in self?.next() }, for: .touchUpInside) nextButton.addAction(UIAction { [weak self] _ in self?.primaryButtonTapped() }, for: .touchUpInside)
let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel]) let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel])
headerStack.axis = .vertical headerStack.axis = .vertical
@@ -136,7 +136,6 @@ final class QuizViewController: UIViewController {
resultLabel.isHidden = true resultLabel.isHidden = true
resultPhoneticLabel.isHidden = true resultPhoneticLabel.isHidden = true
nextButton.isHidden = true
let isSpelling = config.mode == .spelling let isSpelling = config.mode == .spelling
optionsStack.isHidden = isSpelling optionsStack.isHidden = isSpelling
@@ -146,11 +145,16 @@ final class QuizViewController: UIViewController {
if isSpelling { if isSpelling {
spellingHintLabel.text = "\(question.word.word.count) 个字母,补全空缺部分" spellingHintLabel.text = "\(question.word.word.count) 个字母,补全空缺部分"
spellingView.configure(blankedWord: question.blankedWord, blankPositions: question.blankPositions) spellingView.configure(blankedWord: question.blankedWord, blankPositions: question.blankPositions)
spellingView.onComplete = { [weak self] typed in spellingView.onChange = { [weak self] in
self?.submitSpelling(typed, for: question) guard let self else { return }
nextButton.isEnabled = spellingView.isComplete
} }
nextButton.configuration?.title = "确认"
nextButton.isEnabled = false
nextButton.isHidden = false
spellingView.activate() spellingView.activate()
} else { } else {
nextButton.isHidden = true
buildOptionButtons(for: question) buildOptionButtons(for: question)
} }
} }
@@ -221,9 +225,19 @@ final class QuizViewController: UIViewController {
resultLabel.isHidden = false resultLabel.isHidden = false
nextButton.configuration?.title = currentIndex + 1 < questions.count ? "下一题" : "查看成绩" nextButton.configuration?.title = currentIndex + 1 < questions.count ? "下一题" : "查看成绩"
nextButton.isEnabled = true
nextButton.isHidden = false nextButton.isHidden = false
} }
private func primaryButtonTapped() {
if config.mode == .spelling, selectedOption == nil {
guard let question = current, spellingView.isComplete else { return }
submitSpelling(spellingView.typedWord(), for: question)
} else {
next()
}
}
private func next() { private func next() {
if currentIndex + 1 < questions.count { if currentIndex + 1 < questions.count {
currentIndex += 1 currentIndex += 1
@@ -1,7 +1,7 @@
import UIKit import UIKit
final class SpellingInputView: UIView, UITextFieldDelegate { final class SpellingInputView: UIView, UITextFieldDelegate {
var onComplete: ((String) -> Void)? var onChange: (() -> Void)?
private let hiddenField = UITextField() private let hiddenField = UITextField()
private let stack = UIStackView() private let stack = UIStackView()
@@ -9,16 +9,21 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
private var chars: [String] = [] private var chars: [String] = []
private var blankPositions: [Int] = [] private var blankPositions: [Int] = []
private var filledBlanks: [String] = [] private var filledBlanks: [String] = []
private var currentBlankIndex = 0 private var cursorIndex = 0
private var boxViews: [UIView] = [] private var boxViews: [UIView] = []
private var cursorView: UIView? private var cursorView: UIView?
private var inputEnabled = true private var inputEnabled = true
var isComplete: Bool {
!filledBlanks.isEmpty && filledBlanks.allSatisfy { !$0.isEmpty }
}
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
stack.axis = .horizontal stack.axis = .horizontal
stack.spacing = 6 stack.spacing = 6
stack.alignment = .center stack.alignment = .center
stack.isUserInteractionEnabled = true
addSubview(stack) addSubview(stack)
stack.translatesAutoresizingMaskIntoConstraints = false stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@@ -47,11 +52,19 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
chars = blankedWord.map { String($0) } chars = blankedWord.map { String($0) }
self.blankPositions = blankPositions self.blankPositions = blankPositions
filledBlanks = Array(repeating: "", count: blankPositions.count) filledBlanks = Array(repeating: "", count: blankPositions.count)
currentBlankIndex = 0 cursorIndex = 0
inputEnabled = true inputEnabled = true
rebuildBoxes() rebuildBoxes()
} }
func typedWord() -> String {
var rebuilt = chars
for (i, pos) in blankPositions.enumerated() {
rebuilt[pos] = filledBlanks[i]
}
return rebuilt.joined().lowercased()
}
func activate() { func activate() {
guard inputEnabled, !hiddenField.isFirstResponder else { return } guard inputEnabled, !hiddenField.isFirstResponder else { return }
hiddenField.becomeFirstResponder() hiddenField.becomeFirstResponder()
@@ -72,22 +85,17 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard inputEnabled else { return false } guard inputEnabled else { return false }
if string.isEmpty { if string.isEmpty {
if currentBlankIndex > 0 { if cursorIndex > 0 {
currentBlankIndex -= 1 cursorIndex -= 1
filledBlanks[currentBlankIndex] = "" filledBlanks[cursorIndex] = ""
refreshBoxes() refreshBoxes()
onChange?()
} }
} else if let char = string.last, char.isLetter, currentBlankIndex < blankPositions.count { } else if let char = string.last, char.isLetter, cursorIndex < blankPositions.count {
filledBlanks[currentBlankIndex] = String(char).lowercased() filledBlanks[cursorIndex] = String(char).lowercased()
currentBlankIndex += 1 cursorIndex += 1
refreshBoxes() refreshBoxes()
if currentBlankIndex == blankPositions.count { onChange?()
var rebuilt = chars
for (i, pos) in blankPositions.enumerated() {
rebuilt[pos] = filledBlanks[i]
}
onComplete?(rebuilt.joined().lowercased())
}
} }
textField.text = nil textField.text = nil
return false return false
@@ -99,10 +107,12 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
stack.arrangedSubviews.forEach { $0.removeFromSuperview() } stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
boxViews = chars.enumerated().map { index, ch in boxViews = chars.enumerated().map { index, ch in
let box = makeBox() let box = makeBox()
if ch != "_", let label = box.subviews.compactMap({ $0 as? UILabel }).first { box.tag = index
if ch != "_", let label = box.viewWithTag(100) as? UILabel {
label.text = ch.uppercased() label.text = ch.uppercased()
label.isHidden = false label.isHidden = false
} }
box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:))))
stack.addArrangedSubview(box) stack.addArrangedSubview(box)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
box.widthAnchor.constraint(equalToConstant: 36), box.widthAnchor.constraint(equalToConstant: 36),
@@ -113,6 +123,20 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
refreshBoxes() refreshBoxes()
} }
@objc private func boxTapped(_ gesture: UITapGestureRecognizer) {
guard inputEnabled, let box = gesture.view else { return }
let index = box.tag
guard chars[index] == "_", let blankIndex = blankPositions.firstIndex(of: index) else {
activate()
return
}
filledBlanks[blankIndex] = ""
cursorIndex = blankIndex
refreshBoxes()
onChange?()
activate()
}
private func makeBox() -> UIView { private func makeBox() -> UIView {
let box = UIView() let box = UIView()
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15) box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
@@ -147,13 +171,14 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
label?.text = filled.uppercased() label?.text = filled.uppercased()
label?.isHidden = filled.isEmpty label?.isHidden = filled.isEmpty
let isCurrent = inputEnabled && blankIndex == currentBlankIndex let isCurrent = inputEnabled && blankIndex == cursorIndex
box.layer.borderColor = isCurrent ? UIColor.tintColor.cgColor : UIColor.separator.cgColor box.layer.borderColor = isCurrent ? UIColor.tintColor.cgColor : UIColor.separator.cgColor
box.layer.borderWidth = isCurrent ? 2 : 1 box.layer.borderWidth = isCurrent ? 2 : 1
if isCurrent, filled.isEmpty { if isCurrent, filled.isEmpty {
let cursor = UIView() let cursor = UIView()
cursor.backgroundColor = .tintColor cursor.backgroundColor = .tintColor
cursor.isUserInteractionEnabled = false
box.addSubview(cursor) box.addSubview(cursor)
cursor.translatesAutoresizingMaskIntoConstraints = false cursor.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([