From 17d7bc74088a81e21cf6a55758cf659c29fbce42 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 12 Feb 2026 11:37:52 +0100 Subject: [PATCH] Fixed the keyboard dismissal for the NewTaskView view in the app target. --- .../Enumerations/InputFieldFocus.swift | 2 +- App/Sources/Views/ContentView.swift | 4 +- .../Views/NewTask/NewTaskContent.swift | 87 +++++++++++++++++++ App/Sources/Views/NewTaskView.swift | 62 ++++++------- App/Sources/Views/TaskListView.swift | 2 +- 5 files changed, 125 insertions(+), 32 deletions(-) create mode 100644 App/Sources/Views/NewTask/NewTaskContent.swift diff --git a/App/Sources/Enumerations/InputFieldFocus.swift b/App/Sources/Enumerations/InputFieldFocus.swift index 72abfb2..8e14b5d 100644 --- a/App/Sources/Enumerations/InputFieldFocus.swift +++ b/App/Sources/Enumerations/InputFieldFocus.swift @@ -2,7 +2,7 @@ /// /// This enumeration is designed to work with focus state property wrapper, enabling programmatic /// control of keyboard focus between different text input fields when editing todo tasks. -enum InputFieldFocus { +enum InputFieldFocus: Hashable, Sendable { /// The note text field has keyboard focus. /// /// This case represents the optional multi-line note field where users can diff --git a/App/Sources/Views/ContentView.swift b/App/Sources/Views/ContentView.swift index 45d3dbf..640ff40 100644 --- a/App/Sources/Views/ContentView.swift +++ b/App/Sources/Views/ContentView.swift @@ -49,7 +49,9 @@ struct ContentView: View { } } .sheet(isPresented: $viewModel.newTaskShown) { - NewTaskView() + NavigationStack { + NewTaskView() + } } } diff --git a/App/Sources/Views/NewTask/NewTaskContent.swift b/App/Sources/Views/NewTask/NewTaskContent.swift new file mode 100644 index 0000000..a674a30 --- /dev/null +++ b/App/Sources/Views/NewTask/NewTaskContent.swift @@ -0,0 +1,87 @@ +import SwiftUI + +extension NewTaskView { + + /// A container view that wraps form content with tap-to-dismiss keyboard functionality. + /// + /// This view provides a transparent overlay that appears when any text field has focus, + /// allowing users to dismiss the keyboard by tapping anywhere outside the input fields. + /// This improves the user experience by making it easier to exit text editing mode without + /// needing to explicitly tap a "Done" button or similar control. + struct Content: View { + + // MARK: Properties + + /// Binding to track which text field currently has keyboard focus. + @FocusState.Binding + var inputFocus: InputFieldFocus? + + /// The view content to display. + private let content: Content + + // MARK: Initialization + + /// Initializes the new task content with custom form content. + /// + /// - Parameters: + /// - inputFocus: A binding to track keyboard focus state + /// - content: A closure that provides the view content to render + init( + inputFocus: FocusState.Binding, + @ViewBuilder content: () -> Content + ) { + self._inputFocus = inputFocus + self.content = content() + } + + // MARK: Body + + /// The view's body that combines the form content with a dismissal overlay. + /// + /// This approach allows users to tap anywhere on the screen to exit text editing + /// mode, which is particularly useful on iOS where the keyboard can obscure + /// significant screen space. + var body: some View { + ZStack { + content + + if inputFocus != nil { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + inputFocus = nil + } + } + } + } + + } +} + +// MARK: - Previews + +#Preview("New todo task content") { + struct PreviewWrapper: View { + @FocusState + private var inputFocus: InputFieldFocus? + + var body: some View { + NewTaskView.Content(inputFocus: $inputFocus) { + Form { + TextField( + "Title", + text: .constant( + "Sample Task" + ) + ) + .focused( + $inputFocus, + equals: .title + ) + } + } + } + } + + return PreviewWrapper() +} diff --git a/App/Sources/Views/NewTaskView.swift b/App/Sources/Views/NewTaskView.swift index 2979772..ac000d3 100644 --- a/App/Sources/Views/NewTaskView.swift +++ b/App/Sources/Views/NewTaskView.swift @@ -1,12 +1,11 @@ import SwiftData import SwiftUI -/// A view for creating new todo tasks. +/// A view for creating new todo tasks with an intuitive form interface. /// -/// This view provides a form interface for users to input todo task details including: -/// - *Title* (required) -/// - *Note* (optional) -/// - *Due date* (required) +/// This view presents a modal sheet that allows users to create new todo tasks by providing +/// task details through a structured form. The view integrates with SwiftData for persistence +/// and implements smart keyboard focus management for an enhanced input experience. struct NewTaskView: View { // MARK: Properties @@ -18,7 +17,7 @@ struct NewTaskView: View { /// SwiftData model context for saving todo tasks. @Environment(\.modelContext) private var modelContext - + /// Tracks which text field currently has keyboard focus. @FocusState private var inputFocus: InputFieldFocus? @@ -30,6 +29,14 @@ struct NewTaskView: View { // MARK: Initialization /// Initializes a new task view with a fresh view model instance. + /// + /// Creates a new `NewTaskViewModel` with default values: + /// - Empty title + /// - Empty note + /// - Due date set to current date + /// + /// The view model is stored as `@State` to ensure it persists across view updates + /// and maintains its state throughout the view's lifecycle. init() { _viewModel = State(initialValue: .init()) } @@ -38,15 +45,10 @@ struct NewTaskView: View { /// The content and behavior of the view. /// - /// Displays a navigation stack containing a form with sections for: - /// - Task details (title and note fields) - /// - Due date picker with graphical style - /// - /// The toolbar includes: - /// - Cancel button to dismiss without saving - /// - Confirm button to persist the todo and dismiss (disabled when title is empty) + /// Constructs a form interface wrapped in the custom content container that provides + /// tap-to-dismiss keyboard functionality. var body: some View { - NavigationStack { + Content(inputFocus: $inputFocus) { Form { Section("view.new-task.section.task") { TextField( @@ -91,22 +93,22 @@ struct NewTaskView: View { .datePickerStyle(.graphical) } } - .navigationTitle("view.new-task.navigation.title") - .navigationBarTitleDisplayMode(.inline) - .onResignFocus() - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button(role: .cancel) { - dismiss() - } + } + .navigationTitle("view.new-task.navigation.title") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(role: .cancel) { + dismiss() } - ToolbarItem(placement: .confirmationAction) { - Button(role: .confirm) { - viewModel.save(to: modelContext) - dismiss() - } - .disabled(!viewModel.canConfirm) + } + + ToolbarItem(placement: .confirmationAction) { + Button(role: .confirm) { + viewModel.save(to: modelContext) + dismiss() } + .disabled(!viewModel.canConfirm) } } } @@ -119,5 +121,7 @@ struct NewTaskView: View { "New todo task", traits: .emptyContainer ) { - NewTaskView() + NavigationStack { + NewTaskView() + } } diff --git a/App/Sources/Views/TaskListView.swift b/App/Sources/Views/TaskListView.swift index deccbbb..9f8be10 100644 --- a/App/Sources/Views/TaskListView.swift +++ b/App/Sources/Views/TaskListView.swift @@ -30,7 +30,7 @@ struct TaskListView: View { /// - Note: The group determines which tasks are fetched, whether /// completion filtering is available, and whether drag and drop reordering is enabled. init(group: TodoGroup) { - _viewModel = .init(initialValue: ViewModel(group: group)) + _viewModel = .init(initialValue: .init(group: group)) } // MARK: Body