新增默写功能,播放按钮移到导航栏,输入框改为下边框样式
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user