diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d3eb6a8..90c2d1472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Internal: iOS query editor uses a `Binding` focus channel into `SQLHighlightTextView` to dismiss the keyboard before running a query, replacing the `UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder))` call. Keyboard behavior is unchanged - Internal: iOS row detail (edit lifecycle, save SQL build, lazy cell value load, primary key extraction, success-toast auto-dismiss) moves out of the View into `RowDetailViewModel`. The View now keeps only sheet flags and haptic triggers; behavior is unchanged - Internal: iOS connection form (test connection, save, file picker handlers, default port resolution, credential hydration) moves out of the View into `ConnectionFormViewModel`. The View drops from 53 to 5 `@State` properties; behavior is unchanged - Internal: iOS data browser business logic (page load, pagination, sort, filter, search, delete, foreign-key fetch, memory pressure) moves out of the View into `DataBrowserViewModel`. The View drops 30 of its 33 `@State` properties and a dozen private functions; behavior is unchanged diff --git a/TableProMobile/TableProMobile/Views/Components/SQLHighlightTextView.swift b/TableProMobile/TableProMobile/Views/Components/SQLHighlightTextView.swift index f08f70153..985a706fe 100644 --- a/TableProMobile/TableProMobile/Views/Components/SQLHighlightTextView.swift +++ b/TableProMobile/TableProMobile/Views/Components/SQLHighlightTextView.swift @@ -8,9 +8,15 @@ import UIKit struct SQLHighlightTextView: UIViewRepresentable { @Binding var text: String + @Binding var isFocused: Bool private static let font = UIFont.monospacedSystemFont(ofSize: 15, weight: .regular) + init(text: Binding, isFocused: Binding = .constant(false)) { + self._text = text + self._isFocused = isFocused + } + func makeUIView(context: Context) -> UITextView { let textView = UITextView() context.coordinator.textView = textView @@ -40,6 +46,11 @@ struct SQLHighlightTextView: UIViewRepresentable { } context.coordinator.isUpdating = false } + if isFocused, !textView.isFirstResponder { + textView.becomeFirstResponder() + } else if !isFocused, textView.isFirstResponder { + textView.resignFirstResponder() + } } func makeCoordinator() -> Coordinator { Coordinator(self) } @@ -58,6 +69,14 @@ struct SQLHighlightTextView: UIViewRepresentable { parent.text = textView.text } + func textViewDidBeginEditing(_ textView: UITextView) { + if !parent.isFocused { parent.isFocused = true } + } + + func textViewDidEndEditing(_ textView: UITextView) { + if parent.isFocused { parent.isFocused = false } + } + func textStorage( _ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, diff --git a/TableProMobile/TableProMobile/Views/QueryEditorView.swift b/TableProMobile/TableProMobile/Views/QueryEditorView.swift index ef1277f84..ed6adf2f3 100644 --- a/TableProMobile/TableProMobile/Views/QueryEditorView.swift +++ b/TableProMobile/TableProMobile/Views/QueryEditorView.swift @@ -14,6 +14,7 @@ struct QueryEditorView: View { private static let logger = Logger(subsystem: "com.TablePro", category: "QueryEditorView") @State private var query = "" + @State private var editorFocused = false @State private var viewModel = QueryEditorViewModel() @State private var appError: AppError? @State private var isExecuting = false @@ -112,7 +113,7 @@ struct QueryEditorView: View { private var editorSection: some View { VStack(spacing: 0) { - SQLHighlightTextView(text: $query) + SQLHighlightTextView(text: $query, isFocused: $editorFocused) .frame(minHeight: 80, maxHeight: hasResult || appError != nil ? 120 : 250) actionBar @@ -392,7 +393,7 @@ struct QueryEditorView: View { private func executeQueryDirect(_ trimmed: String) async { guard let session else { return } - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + editorFocused = false isExecuting = true executionStartTime = Date() defer {