Files
language4english/LearnEnglish/LearnEnglish/Views/QuizView.swift
T
2026-07-18 21:37:11 +08:00

208 lines
7.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct QuizView: View {
let config: QuizConfig
@Environment(SpeechService.self) private var speech
@Environment(\.dismiss) private var dismiss
@State private var questions: [QuizQuestion]
@State private var currentIndex = 0
@State private var selectedOption: String?
@State private var userInput = ""
@State private var wrongWords: [Word] = []
@State private var finished = false
@State private var startDate = Date.now
init(config: QuizConfig) {
self.config = config
_questions = State(initialValue: QuizGenerator.makeQuestions(mode: config.mode, words: config.words))
}
private var current: QuizQuestion? {
questions.indices.contains(currentIndex) ? questions[currentIndex] : nil
}
var body: some View {
Group {
if finished {
QuizResultView(
total: questions.count,
wrongWords: wrongWords,
duration: Date.now.timeIntervalSince(startDate),
onRestart: restart,
onExit: { dismiss() }
)
} else if let question = current {
questionView(question)
}
}
.navigationTitle(config.mode.rawValue)
.navigationBarTitleDisplayMode(.inline)
}
private func questionView(_ question: QuizQuestion) -> some View {
VStack(spacing: 24) {
VStack(spacing: 8) {
ProgressView(value: Double(currentIndex), total: Double(questions.count))
Text("第 \(currentIndex + 1) / \(questions.count) 题")
.font(.caption)
.foregroundStyle(.secondary)
}
VStack(spacing: 12) {
Text(question.prompt)
.font(.system(size: config.mode == .enToZh ? 44 : 34, weight: .bold))
if config.mode == .enToZh {
Text(question.word.phonetic)
.font(.title3)
.foregroundStyle(.secondary)
Button {
speech.speak(question.prompt)
} label: {
Image(systemName: "speaker.wave.2.fill")
.font(.title2)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
if config.mode == .spelling {
spellingView(question)
} else {
optionsView(question)
}
if let selectedOption {
VStack(spacing: 12) {
if config.mode == .spelling {
Text(selectedOption == question.answer ? "正确" : "正确答案:\(question.answer)")
.font(.title2.weight(.bold))
.foregroundStyle(selectedOption == question.answer ? .green : .red)
Text(question.word.phonetic)
.font(.subheadline)
.foregroundStyle(.secondary)
} else {
Text(selectedOption == question.answer ? "回答正确" : "正确答案:\(question.answer)")
.font(.headline)
.foregroundStyle(selectedOption == question.answer ? .green : .red)
}
Button {
next()
} label: {
Text(currentIndex + 1 < questions.count ? "下一题" : "查看成绩")
.frame(maxWidth: .infinity)
.frame(height: 44)
}
.buttonStyle(.borderedProminent)
.font(.headline)
}
}
Spacer()
}
.padding()
}
private func optionsView(_ question: QuizQuestion) -> some View {
VStack(spacing: 12) {
ForEach(question.options, id: \.self) { option in
Button {
select(option, for: question)
} label: {
VStack(spacing: 4) {
Text(option)
.font(.headline)
if config.mode == .zhToEn, let w = config.words.first(where: { $0.word == option }) {
Text(w.phonetic)
.font(.caption)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
}
.buttonStyle(.bordered)
.tint(optionColor(option, for: question))
.disabled(selectedOption != nil)
}
}
}
private func spellingView(_ question: QuizQuestion) -> some View {
VStack(spacing: 20) {
Text(question.blankedWord)
.font(.system(size: 44, weight: .bold, design: .monospaced))
.tracking(6)
Text("\(question.word.word.count) 个字母")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack(spacing: 12) {
TextField("填写完整单词", text: $userInput)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.disableAutocorrection(true)
.disabled(selectedOption != nil)
Button("确认") {
let answer = userInput.trimmingCharacters(in: .whitespaces).lowercased()
guard !answer.isEmpty else { return }
selectedOption = answer
if answer != question.answer {
wrongWords.append(question.word)
}
}
.buttonStyle(.borderedProminent)
.disabled(userInput.trimmingCharacters(in: .whitespaces).isEmpty || selectedOption != nil)
}
}
}
private func optionColor(_ option: String, for question: QuizQuestion) -> Color {
guard let selectedOption else { return .accentColor }
if option == question.answer { return .green }
if option == selectedOption { return .red }
return .gray
}
private func select(_ option: String, for question: QuizQuestion) {
guard selectedOption == nil else { return }
selectedOption = option
if option != question.answer {
wrongWords.append(question.word)
}
}
private func next() {
if currentIndex + 1 < questions.count {
currentIndex += 1
selectedOption = nil
userInput = ""
} else {
finished = true
}
}
private func restart() {
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
currentIndex = 0
selectedOption = nil
userInput = ""
wrongWords = []
finished = false
startDate = .now
}
}
#Preview {
NavigationStack {
QuizView(config: QuizConfig(mode: .enToZh, words: [
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。"),
Word(word: "blue", phonetic: "/bluː/", meaning: "蓝色", example: "The sky is blue.", exampleMeaning: "天空是蓝色的。"),
Word(word: "green", phonetic: "/ɡriːn/", meaning: "绿色", example: "The grass is green.", exampleMeaning: "草地是绿色的。"),
Word(word: "yellow", phonetic: "/ˈjeloʊ/", meaning: "黄色", example: "The banana is yellow.", exampleMeaning: "香蕉是黄色的。")
]))
.environment(SpeechService())
}
}