diff --git a/LearnEnglish/LearnEnglish/Views/QuizView.swift b/LearnEnglish/LearnEnglish/Views/QuizView.swift index 22bf03a..fb0e78c 100644 --- a/LearnEnglish/LearnEnglish/Views/QuizView.swift +++ b/LearnEnglish/LearnEnglish/Views/QuizView.swift @@ -9,8 +9,7 @@ struct QuizView: View { @State private var questions: [QuizQuestion] @State private var currentIndex = 0 @State private var selectedOption: String? - @State private var filledBlanks: [String] = [] - @State private var currentBlankIndex = 0 + @State private var userInput = "" @State private var wrongWords: [Word] = [] @State private var finished = false @State private var startDate = Date.now @@ -40,25 +39,6 @@ struct QuizView: View { } .navigationTitle(config.mode.rawValue) .navigationBarTitleDisplayMode(.inline) - .onChange(of: current?.id) { _, _ in - resetBlanks() - } - .onChange(of: currentBlankIndex) { _, _ in - guard config.mode == .spelling, let question = current else { return } - if currentBlankIndex == question.blankPositions.count { - let typed = filledBlanks.joined() - selectedOption = typed - if typed != question.answer { - wrongWords.append(question.word) - } - } - } - } - - private func resetBlanks() { - guard config.mode == .spelling, let question = current else { return } - filledBlanks = Array(repeating: "", count: question.blankPositions.count) - currentBlankIndex = 0 } private func questionView(_ question: QuizQuestion) -> some View { @@ -151,18 +131,30 @@ struct QuizView: View { } private func spellingView(_ question: QuizQuestion) -> some View { - VStack(spacing: 16) { + 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) - SpellingWordInput( - blankedWord: question.blankedWord, - blankPositions: question.blankPositions, - currentBlankIndex: $currentBlankIndex, - filledBlanks: $filledBlanks - ) - .disabled(selectedOption != nil) - .opacity(selectedOption != nil ? 0.6 : 1) + 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) + } } } @@ -185,6 +177,7 @@ struct QuizView: View { if currentIndex + 1 < questions.count { currentIndex += 1 selectedOption = nil + userInput = "" } else { finished = true } @@ -194,8 +187,7 @@ struct QuizView: View { questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words) currentIndex = 0 selectedOption = nil - filledBlanks = [] - currentBlankIndex = 0 + userInput = "" wrongWords = [] finished = false startDate = .now diff --git a/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift b/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift deleted file mode 100644 index 95730d6..0000000 --- a/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift +++ /dev/null @@ -1,137 +0,0 @@ -import SwiftUI -import UIKit - -struct KeystrokeCaptureField: UIViewRepresentable { - let onChar: (Character) -> Void - let onBackspace: () -> Void - let startActive: Bool - - func makeUIView(context: Context) -> UITextField { - let tf = UITextField() - tf.delegate = context.coordinator - tf.autocorrectionType = .no - tf.autocapitalizationType = .none - tf.spellCheckingType = .no - context.coordinator.textField = tf - return tf - } - - func makeCoordinator() -> Coordinator { - Coordinator(onChar: onChar, onBackspace: onBackspace) - } - - func updateUIView(_ uiView: UITextField, context: Context) { - if startActive && !context.coordinator.didFocus { - uiView.becomeFirstResponder() - context.coordinator.didFocus = true - } - } - - class Coordinator: NSObject, UITextFieldDelegate { - let onChar: (Character) -> Void - let onBackspace: () -> Void - weak var textField: UITextField? - var didFocus = false - - init(onChar: @escaping (Character) -> Void, onBackspace: @escaping () -> Void) { - self.onChar = onChar - self.onBackspace = onBackspace - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if string.isEmpty { - onBackspace() - } else if let char = string.last { - onChar(char) - } - textField.text = nil - return false - } - } -} - -struct BlinkingCursor: View { - var body: some View { - TimelineView(.periodic(from: .now, by: 0.6)) { timeline in - let visible = Int(timeline.date.timeIntervalSinceReferenceDate / 0.6) % 2 == 0 - Rectangle() - .fill(.tint) - .frame(width: 2, height: 22) - .opacity(visible ? 1 : 0) - } - } -} - -struct SpellingWordInput: View { - let blankedWord: String - let blankPositions: [Int] - @Binding var currentBlankIndex: Int - @Binding var filledBlanks: [String] - - private let chars: [String] - - init(blankedWord: String, blankPositions: [Int], currentBlankIndex: Binding, filledBlanks: Binding<[String]>) { - self.blankedWord = blankedWord - self.blankPositions = blankPositions - self._currentBlankIndex = currentBlankIndex - self._filledBlanks = filledBlanks - self.chars = blankedWord.map { String($0) } - } - - var body: some View { - KeystrokeCaptureField( - onChar: { char in - guard currentBlankIndex < blankPositions.count else { return } - filledBlanks[currentBlankIndex] = String(char).lowercased() - currentBlankIndex += 1 - }, - onBackspace: { - guard currentBlankIndex > 0 else { return } - currentBlankIndex -= 1 - filledBlanks[currentBlankIndex] = "" - }, - startActive: true - ) - .frame(width: 0, height: 0) - .overlay(alignment: .top) { - HStack(spacing: 4) { - ForEach(0.. some View { - let ch = chars[index] - if ch == "_" { - let blankIndex = blankPositions.firstIndex(of: index) ?? index - let isCurrent = blankIndex == currentBlankIndex - let filled = blankIndex < filledBlanks.count ? filledBlanks[blankIndex] : nil - ZStack { - RoundedRectangle(cornerRadius: 6) - .fill(.quaternary.opacity(0.2)) - .frame(width: 28, height: 40) - RoundedRectangle(cornerRadius: 6) - .stroke(.quaternary, lineWidth: 1) - .frame(width: 28, height: 40) - .opacity(isCurrent ? 0 : 1) - RoundedRectangle(cornerRadius: 6) - .stroke(.tint, lineWidth: 2) - .frame(width: 28, height: 40) - .opacity(isCurrent ? 1 : 0) - if let filled, !filled.isEmpty { - Text(filled.uppercased()) - .font(.title2.weight(.bold)) - } else if isCurrent { - BlinkingCursor() - } - } - } else { - Text(ch.uppercased()) - .font(.title2.weight(.bold)) - .frame(width: 22) - } - } -}