From 71dfa297ed6f349b5924b19b90e3d0a9cf8c2df3 Mon Sep 17 00:00:00 2001 From: fish Date: Sat, 18 Jul 2026 22:42:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=BC=E5=86=99=E6=94=B9=E4=B8=BA=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E6=8C=89=E9=92=AE=E6=89=8B=E5=8A=A8=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=8C=E7=82=B9=E5=87=BB=E5=B7=B2=E5=A1=AB=E5=AD=97=E6=AF=8D?= =?UTF-8?q?=E5=8F=AF=E5=88=A0=E9=99=A4=E9=87=8D=E8=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .../Views/QuizViewController.swift | 22 +++++-- .../Views/SpellingInputView.swift | 61 +++++++++++++------ 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/LearnEnglish/LearnEnglish/Views/QuizViewController.swift b/LearnEnglish/LearnEnglish/Views/QuizViewController.swift index 9f1c71c..02c73e9 100644 --- a/LearnEnglish/LearnEnglish/Views/QuizViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/QuizViewController.swift @@ -89,7 +89,7 @@ final class QuizViewController: UIViewController { var nextConfig = UIButton.Configuration.borderedProminent() nextConfig.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16) 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]) headerStack.axis = .vertical @@ -136,7 +136,6 @@ final class QuizViewController: UIViewController { resultLabel.isHidden = true resultPhoneticLabel.isHidden = true - nextButton.isHidden = true let isSpelling = config.mode == .spelling optionsStack.isHidden = isSpelling @@ -146,11 +145,16 @@ final class QuizViewController: UIViewController { if isSpelling { spellingHintLabel.text = "\(question.word.word.count) 个字母,补全空缺部分" spellingView.configure(blankedWord: question.blankedWord, blankPositions: question.blankPositions) - spellingView.onComplete = { [weak self] typed in - self?.submitSpelling(typed, for: question) + spellingView.onChange = { [weak self] in + guard let self else { return } + nextButton.isEnabled = spellingView.isComplete } + nextButton.configuration?.title = "确认" + nextButton.isEnabled = false + nextButton.isHidden = false spellingView.activate() } else { + nextButton.isHidden = true buildOptionButtons(for: question) } } @@ -221,9 +225,19 @@ final class QuizViewController: UIViewController { resultLabel.isHidden = false nextButton.configuration?.title = currentIndex + 1 < questions.count ? "下一题" : "查看成绩" + nextButton.isEnabled = true 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() { if currentIndex + 1 < questions.count { currentIndex += 1 diff --git a/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift b/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift index 75cfd79..7692342 100644 --- a/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift +++ b/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift @@ -1,7 +1,7 @@ import UIKit final class SpellingInputView: UIView, UITextFieldDelegate { - var onComplete: ((String) -> Void)? + var onChange: (() -> Void)? private let hiddenField = UITextField() private let stack = UIStackView() @@ -9,16 +9,21 @@ final class SpellingInputView: UIView, UITextFieldDelegate { private var chars: [String] = [] private var blankPositions: [Int] = [] private var filledBlanks: [String] = [] - private var currentBlankIndex = 0 + private var cursorIndex = 0 private var boxViews: [UIView] = [] private var cursorView: UIView? private var inputEnabled = true + var isComplete: Bool { + !filledBlanks.isEmpty && filledBlanks.allSatisfy { !$0.isEmpty } + } + override init(frame: CGRect) { super.init(frame: frame) stack.axis = .horizontal stack.spacing = 6 stack.alignment = .center + stack.isUserInteractionEnabled = true addSubview(stack) stack.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -47,11 +52,19 @@ final class SpellingInputView: UIView, UITextFieldDelegate { chars = blankedWord.map { String($0) } self.blankPositions = blankPositions filledBlanks = Array(repeating: "", count: blankPositions.count) - currentBlankIndex = 0 + cursorIndex = 0 inputEnabled = true rebuildBoxes() } + func typedWord() -> String { + var rebuilt = chars + for (i, pos) in blankPositions.enumerated() { + rebuilt[pos] = filledBlanks[i] + } + return rebuilt.joined().lowercased() + } + func activate() { guard inputEnabled, !hiddenField.isFirstResponder else { return } hiddenField.becomeFirstResponder() @@ -72,22 +85,17 @@ final class SpellingInputView: UIView, 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] = "" + if cursorIndex > 0 { + cursorIndex -= 1 + filledBlanks[cursorIndex] = "" refreshBoxes() + onChange?() } - } else if let char = string.last, char.isLetter, currentBlankIndex < blankPositions.count { - filledBlanks[currentBlankIndex] = String(char).lowercased() - currentBlankIndex += 1 + } else if let char = string.last, char.isLetter, cursorIndex < blankPositions.count { + filledBlanks[cursorIndex] = String(char).lowercased() + cursorIndex += 1 refreshBoxes() - if currentBlankIndex == blankPositions.count { - var rebuilt = chars - for (i, pos) in blankPositions.enumerated() { - rebuilt[pos] = filledBlanks[i] - } - onComplete?(rebuilt.joined().lowercased()) - } + onChange?() } textField.text = nil return false @@ -99,10 +107,12 @@ final class SpellingInputView: UIView, UITextFieldDelegate { 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 { + box.tag = index + if ch != "_", let label = box.viewWithTag(100) as? UILabel { label.text = ch.uppercased() label.isHidden = false } + box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:)))) stack.addArrangedSubview(box) NSLayoutConstraint.activate([ box.widthAnchor.constraint(equalToConstant: 36), @@ -113,6 +123,20 @@ final class SpellingInputView: UIView, UITextFieldDelegate { 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 { let box = UIView() box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15) @@ -147,13 +171,14 @@ final class SpellingInputView: UIView, UITextFieldDelegate { label?.text = filled.uppercased() 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.borderWidth = isCurrent ? 2 : 1 if isCurrent, filled.isEmpty { let cursor = UIView() cursor.backgroundColor = .tintColor + cursor.isUserInteractionEnabled = false box.addSubview(cursor) cursor.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([