新增默写功能,播放按钮移到导航栏,输入框改为下边框样式

This commit is contained in:
2026-07-18 22:59:58 +08:00
parent a0141ecab0
commit 682e7927a9
4 changed files with 119 additions and 6 deletions
+3 -2
View File
@@ -4,6 +4,7 @@ enum QuizMode: String, Hashable {
case enToZh = "英选汉" case enToZh = "英选汉"
case zhToEn = "汉选英" case zhToEn = "汉选英"
case spelling = "拼写" case spelling = "拼写"
case dictation = "默写"
} }
struct QuizSetupRequest: Hashable { struct QuizSetupRequest: Hashable {
@@ -48,7 +49,7 @@ enum QuizGenerator {
switch mode { switch mode {
case .enToZh: return word.word case .enToZh: return word.word
case .zhToEn: return word.meaning case .zhToEn: return word.meaning
case .spelling: return word.meaning case .spelling, .dictation: return word.meaning
} }
} }
@@ -56,7 +57,7 @@ enum QuizGenerator {
switch mode { switch mode {
case .enToZh: return word.meaning case .enToZh: return word.meaning
case .zhToEn: return word.word case .zhToEn: return word.word
case .spelling: return word.word case .spelling, .dictation: return word.word
} }
} }
@@ -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 = "" }
}
@@ -17,6 +17,7 @@ final class QuizViewController: UIViewController {
private let speakerButton = UIButton(type: .system) private let speakerButton = UIButton(type: .system)
private let optionsStack = UIStackView() private let optionsStack = UIStackView()
private let spellingView = SpellingInputView() private let spellingView = SpellingInputView()
private let dictationView = DictationInputView()
private let spellingHintLabel = UILabel() private let spellingHintLabel = UILabel()
private let resultLabel = UILabel() private let resultLabel = UILabel()
private let resultPhoneticLabel = UILabel() private let resultPhoneticLabel = UILabel()
@@ -48,6 +49,8 @@ final class QuizViewController: UIViewController {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if config.mode == .spelling, selectedOption == nil { if config.mode == .spelling, selectedOption == nil {
spellingView.activate() spellingView.activate()
} else if config.mode == .dictation, selectedOption == nil {
dictationView.activate()
} }
} }
@@ -91,6 +94,13 @@ final class QuizViewController: UIViewController {
nextButton.configuration = nextConfig nextButton.configuration = nextConfig
nextButton.addAction(UIAction { [weak self] _ in self?.primaryButtonTapped() }, for: .touchUpInside) 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]) let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel])
headerStack.axis = .vertical headerStack.axis = .vertical
headerStack.spacing = 8 headerStack.spacing = 8
@@ -100,7 +110,7 @@ final class QuizViewController: UIViewController {
promptStack.spacing = 12 promptStack.spacing = 12
promptStack.alignment = .center promptStack.alignment = .center
let inputStack = UIStackView(arrangedSubviews: [optionsStack, spellingView, spellingHintLabel]) let inputStack = UIStackView(arrangedSubviews: [optionsStack, spellingView, dictationView, spellingHintLabel])
inputStack.axis = .vertical inputStack.axis = .vertical
inputStack.spacing = 16 inputStack.spacing = 16
@@ -138,8 +148,11 @@ final class QuizViewController: UIViewController {
resultPhoneticLabel.isHidden = true resultPhoneticLabel.isHidden = true
let isSpelling = config.mode == .spelling let isSpelling = config.mode == .spelling
optionsStack.isHidden = isSpelling let isDictation = config.mode == .dictation
let isChoice = !isSpelling && !isDictation
optionsStack.isHidden = !isChoice
spellingView.isHidden = !isSpelling spellingView.isHidden = !isSpelling
dictationView.isHidden = !isDictation
spellingHintLabel.isHidden = !isSpelling spellingHintLabel.isHidden = !isSpelling
if isSpelling { if isSpelling {
@@ -153,6 +166,20 @@ final class QuizViewController: UIViewController {
nextButton.isEnabled = false nextButton.isEnabled = false
nextButton.isHidden = false nextButton.isHidden = false
spellingView.activate() 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 { } else {
nextButton.isHidden = true nextButton.isHidden = true
buildOptionButtons(for: question) buildOptionButtons(for: question)
@@ -213,8 +240,18 @@ final class QuizViewController: UIViewController {
showResult(typed == question.answer, for: question) 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) { private func showResult(_ correct: Bool, for question: QuizQuestion) {
if config.mode == .spelling { if config.mode == .spelling || config.mode == .dictation {
resultLabel.text = correct ? "正确" : "正确答案:\(question.answer)" resultLabel.text = correct ? "正确" : "正确答案:\(question.answer)"
resultPhoneticLabel.text = question.word.phonetic resultPhoneticLabel.text = question.word.phonetic
resultPhoneticLabel.isHidden = false resultPhoneticLabel.isHidden = false
@@ -229,10 +266,18 @@ final class QuizViewController: UIViewController {
nextButton.isHidden = false nextButton.isHidden = false
} }
@objc private func speakCurrentWord() {
guard let question = current else { return }
speech.speak(question.answer)
}
private func primaryButtonTapped() { private func primaryButtonTapped() {
if config.mode == .spelling, selectedOption == nil { if config.mode == .spelling, selectedOption == nil {
guard let question = current, spellingView.isComplete else { return } guard let question = current, spellingView.isComplete else { return }
submitSpelling(spellingView.typedWord(), for: question) 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 { } else {
next() next()
} }
@@ -273,6 +318,8 @@ final class QuizViewController: UIViewController {
showQuestion() showQuestion()
if config.mode == .spelling { if config.mode == .spelling {
spellingView.activate() spellingView.activate()
} else if config.mode == .dictation {
dictationView.activate()
} }
} }
} }
@@ -20,7 +20,8 @@ final class WordListViewController: UITableViewController {
let quizMenu = UIMenu(title: "", children: [ let quizMenu = UIMenu(title: "", children: [
UIAction(title: QuizMode.enToZh.rawValue) { [weak self] _ in self?.startQuiz(.enToZh) }, 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.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) navigationItem.rightBarButtonItem = UIBarButtonItem(title: "测验", menu: quizMenu)
} }