From 682e7927a9e7f3510c2e52a31eba42ee3be13ac0 Mon Sep 17 00:00:00 2001 From: fish Date: Sat, 18 Jul 2026 22:59:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=BB=98=E5=86=99=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=92=AD=E6=94=BE=E6=8C=89=E9=92=AE=E7=A7=BB?= =?UTF-8?q?=E5=88=B0=E5=AF=BC=E8=88=AA=E6=A0=8F=EF=BC=8C=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86=E6=94=B9=E4=B8=BA=E4=B8=8B=E8=BE=B9=E6=A1=86=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LearnEnglish/LearnEnglish/Models/Quiz.swift | 5 +- .../Views/DictationInputView.swift | 64 +++++++++++++++++++ .../Views/QuizViewController.swift | 53 ++++++++++++++- .../Views/WordListViewController.swift | 3 +- 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 LearnEnglish/LearnEnglish/Views/DictationInputView.swift diff --git a/LearnEnglish/LearnEnglish/Models/Quiz.swift b/LearnEnglish/LearnEnglish/Models/Quiz.swift index 4e14821..5276dfe 100644 --- a/LearnEnglish/LearnEnglish/Models/Quiz.swift +++ b/LearnEnglish/LearnEnglish/Models/Quiz.swift @@ -4,6 +4,7 @@ enum QuizMode: String, Hashable { case enToZh = "英选汉" case zhToEn = "汉选英" case spelling = "拼写" + case dictation = "默写" } struct QuizSetupRequest: Hashable { @@ -48,7 +49,7 @@ enum QuizGenerator { switch mode { case .enToZh: return word.word case .zhToEn: return word.meaning - case .spelling: return word.meaning + case .spelling, .dictation: return word.meaning } } @@ -56,7 +57,7 @@ enum QuizGenerator { switch mode { case .enToZh: return word.meaning case .zhToEn: return word.word - case .spelling: return word.word + case .spelling, .dictation: return word.word } } diff --git a/LearnEnglish/LearnEnglish/Views/DictationInputView.swift b/LearnEnglish/LearnEnglish/Views/DictationInputView.swift new file mode 100644 index 0000000..ff11179 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Views/DictationInputView.swift @@ -0,0 +1,64 @@ +import UIKit + +final class DictationInputView: UIView, UITextFieldDelegate { + var onChange: (() -> Void)? + var onSubmit: (() -> Void)? + + private let textField = UITextField() + private let bottomLine = UIView() + + var text: String { textField.text?.trimmingCharacters(in: .whitespaces) ?? "" } + var isEmpty: Bool { text.isEmpty } + + override init(frame: CGRect) { + super.init(frame: frame) + textField.borderStyle = .none + textField.font = .systemFont(ofSize: 28, weight: .bold) + textField.textAlignment = .center + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + textField.spellCheckingType = .no + textField.keyboardType = .asciiCapable + textField.placeholder = "输入完整单词" + textField.delegate = self + textField.returnKeyType = .done + textField.addTarget(self, action: #selector(textChanged), for: .editingChanged) + + bottomLine.backgroundColor = .separator + bottomLine.translatesAutoresizingMaskIntoConstraints = false + + addSubview(textField) + addSubview(bottomLine) + textField.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + textField.topAnchor.constraint(equalTo: topAnchor), + textField.bottomAnchor.constraint(equalTo: bottomAnchor), + textField.centerXAnchor.constraint(equalTo: centerXAnchor), + textField.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 16), + textField.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16), + textField.widthAnchor.constraint(greaterThanOrEqualToConstant: 200), + textField.heightAnchor.constraint(equalToConstant: 48), + + bottomLine.leadingAnchor.constraint(equalTo: textField.leadingAnchor), + bottomLine.trailingAnchor.constraint(equalTo: textField.trailingAnchor), + bottomLine.bottomAnchor.constraint(equalTo: textField.bottomAnchor), + bottomLine.heightAnchor.constraint(equalToConstant: 1.0 / UITraitCollection().displayScale), + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + @objc private func textChanged() { + onChange?() + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + onSubmit?() + return false + } + + func activate() { textField.becomeFirstResponder() } + func deactivate() { textField.resignFirstResponder() } + func reset() { textField.text = "" } +} diff --git a/LearnEnglish/LearnEnglish/Views/QuizViewController.swift b/LearnEnglish/LearnEnglish/Views/QuizViewController.swift index 02c73e9..6ea1a15 100644 --- a/LearnEnglish/LearnEnglish/Views/QuizViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/QuizViewController.swift @@ -17,6 +17,7 @@ final class QuizViewController: UIViewController { private let speakerButton = UIButton(type: .system) private let optionsStack = UIStackView() private let spellingView = SpellingInputView() + private let dictationView = DictationInputView() private let spellingHintLabel = UILabel() private let resultLabel = UILabel() private let resultPhoneticLabel = UILabel() @@ -48,6 +49,8 @@ final class QuizViewController: UIViewController { super.viewDidAppear(animated) if config.mode == .spelling, selectedOption == nil { spellingView.activate() + } else if config.mode == .dictation, selectedOption == nil { + dictationView.activate() } } @@ -91,6 +94,13 @@ final class QuizViewController: UIViewController { nextButton.configuration = nextConfig nextButton.addAction(UIAction { [weak self] _ in self?.primaryButtonTapped() }, for: .touchUpInside) + navigationItem.rightBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "speaker.wave.2.fill"), + style: .plain, + target: self, + action: #selector(speakCurrentWord) + ) + let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel]) headerStack.axis = .vertical headerStack.spacing = 8 @@ -100,7 +110,7 @@ final class QuizViewController: UIViewController { promptStack.spacing = 12 promptStack.alignment = .center - let inputStack = UIStackView(arrangedSubviews: [optionsStack, spellingView, spellingHintLabel]) + let inputStack = UIStackView(arrangedSubviews: [optionsStack, spellingView, dictationView, spellingHintLabel]) inputStack.axis = .vertical inputStack.spacing = 16 @@ -138,8 +148,11 @@ final class QuizViewController: UIViewController { resultPhoneticLabel.isHidden = true let isSpelling = config.mode == .spelling - optionsStack.isHidden = isSpelling + let isDictation = config.mode == .dictation + let isChoice = !isSpelling && !isDictation + optionsStack.isHidden = !isChoice spellingView.isHidden = !isSpelling + dictationView.isHidden = !isDictation spellingHintLabel.isHidden = !isSpelling if isSpelling { @@ -153,6 +166,20 @@ final class QuizViewController: UIViewController { nextButton.isEnabled = false nextButton.isHidden = false spellingView.activate() + } else if isDictation { + dictationView.reset() + dictationView.onChange = { [weak self] in + guard let self else { return } + nextButton.isEnabled = !dictationView.isEmpty + } + dictationView.onSubmit = { [weak self] in + guard let self else { return } + primaryButtonTapped() + } + nextButton.configuration?.title = "确认" + nextButton.isEnabled = false + nextButton.isHidden = false + dictationView.activate() } else { nextButton.isHidden = true buildOptionButtons(for: question) @@ -213,8 +240,18 @@ final class QuizViewController: UIViewController { showResult(typed == question.answer, for: question) } + private func submitDictation(_ typed: String, for question: QuizQuestion) { + guard selectedOption == nil else { return } + selectedOption = typed + if typed != question.answer { + wrongWords.append(question.word) + } + dictationView.deactivate() + showResult(typed == question.answer, for: question) + } + private func showResult(_ correct: Bool, for question: QuizQuestion) { - if config.mode == .spelling { + if config.mode == .spelling || config.mode == .dictation { resultLabel.text = correct ? "正确" : "正确答案:\(question.answer)" resultPhoneticLabel.text = question.word.phonetic resultPhoneticLabel.isHidden = false @@ -229,10 +266,18 @@ final class QuizViewController: UIViewController { nextButton.isHidden = false } + @objc private func speakCurrentWord() { + guard let question = current else { return } + speech.speak(question.answer) + } + private func primaryButtonTapped() { if config.mode == .spelling, selectedOption == nil { guard let question = current, spellingView.isComplete else { return } submitSpelling(spellingView.typedWord(), for: question) + } else if config.mode == .dictation, selectedOption == nil { + guard let question = current, !dictationView.isEmpty else { return } + submitDictation(dictationView.text, for: question) } else { next() } @@ -273,6 +318,8 @@ final class QuizViewController: UIViewController { showQuestion() if config.mode == .spelling { spellingView.activate() + } else if config.mode == .dictation { + dictationView.activate() } } } diff --git a/LearnEnglish/LearnEnglish/Views/WordListViewController.swift b/LearnEnglish/LearnEnglish/Views/WordListViewController.swift index 77aad5f..353df83 100644 --- a/LearnEnglish/LearnEnglish/Views/WordListViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/WordListViewController.swift @@ -20,7 +20,8 @@ final class WordListViewController: UITableViewController { let quizMenu = UIMenu(title: "", children: [ UIAction(title: QuizMode.enToZh.rawValue) { [weak self] _ in self?.startQuiz(.enToZh) }, UIAction(title: QuizMode.zhToEn.rawValue) { [weak self] _ in self?.startQuiz(.zhToEn) }, - UIAction(title: QuizMode.spelling.rawValue) { [weak self] _ in self?.startQuiz(.spelling) } + UIAction(title: QuizMode.spelling.rawValue) { [weak self] _ in self?.startQuiz(.spelling) }, + UIAction(title: QuizMode.dictation.rawValue) { [weak self] _ in self?.startQuiz(.dictation) } ]) navigationItem.rightBarButtonItem = UIBarButtonItem(title: "测验", menu: quizMenu) }