diff --git a/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift b/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift index e257822..95730d6 100644 --- a/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift +++ b/LearnEnglish/LearnEnglish/Views/SpellingWordInput.swift @@ -1,11 +1,10 @@ import SwiftUI import UIKit -// Captures keystrokes one at a time via hidden UITextField delegate struct KeystrokeCaptureField: UIViewRepresentable { let onChar: (Character) -> Void let onBackspace: () -> Void - @Binding var isActive: Bool + let startActive: Bool func makeUIView(context: Context) -> UITextField { let tf = UITextField() @@ -13,6 +12,7 @@ struct KeystrokeCaptureField: UIViewRepresentable { tf.autocorrectionType = .no tf.autocapitalizationType = .none tf.spellCheckingType = .no + context.coordinator.textField = tf return tf } @@ -21,16 +21,17 @@ struct KeystrokeCaptureField: UIViewRepresentable { } func updateUIView(_ uiView: UITextField, context: Context) { - if isActive { + if startActive && !context.coordinator.didFocus { uiView.becomeFirstResponder() - } else { - uiView.resignFirstResponder() + 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 @@ -49,7 +50,6 @@ struct KeystrokeCaptureField: UIViewRepresentable { } } -// Blinking cursor on the active blank struct BlinkingCursor: View { var body: some View { TimelineView(.periodic(from: .now, by: 0.6)) { timeline in @@ -62,13 +62,11 @@ struct BlinkingCursor: View { } } -// Interactive word input with per-blank slots struct SpellingWordInput: View { let blankedWord: String let blankPositions: [Int] @Binding var currentBlankIndex: Int @Binding var filledBlanks: [String] - @State private var isFocused = false private let chars: [String] @@ -81,67 +79,59 @@ struct SpellingWordInput: View { } var body: some View { - VStack(spacing: 8) { - 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] = "" - }, - isActive: $isFocused - ) - .frame(width: 0, height: 0) - + 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(Array(chars.enumerated()), id: \.offset) { index, ch in - if ch == "_" { - blankSlot(index: index) - } else { - Text(ch.uppercased()) - .font(.title2.weight(.bold)) - .frame(width: 22) - } + ForEach(0.. some View { - let blankIndex = blankPositions.firstIndex(of: index) ?? 0 - let isCurrent = blankIndex == currentBlankIndex && isFocused - 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) - if isCurrent { + private func charView(at index: Int) -> 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() + } } - - 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) } } }