Task edition improvements (#9)

This PR contains the work done to improve upon the editing experience on the **Task list item and the **New task** views.

Reviewed-on: rock-n-code/todos-app#9
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #9.
This commit is contained in:
2026-02-12 01:21:19 +00:00
committed by javier
parent bbd7ec264b
commit 52376695a4
11 changed files with 355 additions and 47 deletions
@@ -0,0 +1,81 @@
import SwiftUI
/// A view modifier that clears text and removes focus when return is pressed on empty content.
///
/// This modifier monitors text changes and provides smart handling of newlines:
/// - Empty text is allowed normally
/// - Newlines at the end of whitespace-only text clear the field and remove focus
/// - Multi-line content with actual text is preserved normally
struct OnEmptyNewlineResignFocusModifier<Field: Hashable>: ViewModifier {
// MARK: Properties
/// Binding to the text being edited.
@Binding
private var text: String
/// Binding to the focus state.
private let focusState: FocusState<Field?>.Binding
// MARK: Initializers
/// Creates a modifier that clears text on empty newline.
///
/// - Parameters:
/// - text: A binding to the text being edited.
/// - focusState: A binding to the focus state.
init(
text: Binding<String>,
focusState: FocusState<Field?>.Binding
) {
self._text = text
self.focusState = focusState
}
// MARK: Body
/// The content and behavior of the view modifier.
func body(content: Content) -> some View {
content
.onChange(of: text) { oldValue, newValue in
guard newValue.isEmpty == false else {
return
}
if newValue.last == .newline {
if newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
text = ""
focusState.wrappedValue = nil
}
}
}
}
}
// MARK: - View Extension
extension View {
/// Clears the text and removes focus when return is pressed on empty content.
///
/// This modifier allows users to dismiss an optional text field by pressing return
/// when the field is empty or contains only whitespace. Multi-line content is preserved normally.
///
/// - Parameters:
/// - focusState: A binding to the focus state.
/// - text: A binding to the text being edited.
/// - Returns: A modified view with clear-on-empty behavior.
func onEmptyNewlineResignFocus<Field: Hashable>(
text: Binding<String>,
from focusState: FocusState<Field?>.Binding
) -> some View {
modifier(
OnEmptyNewlineResignFocusModifier(
text: text,
focusState: focusState
)
)
}
}
@@ -0,0 +1,86 @@
import SwiftUI
/// A view modifier that intercepts newlines in a text field and transfers focus to another field.
///
/// This modifier monitors text changes in a `TextField` component and prevents newlines
/// from being added. Instead, when a newline is detected, it updates the focus state to move to
/// a specified field.
struct OnNewlineFocusModifier<Field: Hashable>: ViewModifier {
// MARK: Properties
/// Binding to the text being edited.
@Binding
private var text: String
/// Binding to the focus state.
private let focusState: FocusState<Field?>.Binding
/// The field to focus when a newline is detected.
private let targetField: Field
// MARK: Initializers
/// Creates a modifier that transfers focus on newline.
///
/// - Parameters:
/// - text: A binding to the text being edited.
/// - focusState: A binding to the focus state.
/// - targetField: The field to focus when a newline is detected.
init(
text: Binding<String>,
focusState: FocusState<Field?>.Binding,
targetField: Field
) {
self._text = text
self.focusState = focusState
self.targetField = targetField
}
// MARK: Body
/// The content and behavior of the view modifier.
func body(content: Content) -> some View {
content
.onChange(of: text) { oldValue, newValue in
guard newValue.contains(.newline) else {
return
}
text = oldValue
focusState.wrappedValue = targetField
}
}
}
// MARK: - View Extension
extension View {
/// Transfers focus to another field when the user presses return in a text field.
///
/// This modifier prevents newlines from being added and instead moves keyboard focus
/// to the specified field. Useful for creating a form-like flow between text fields.
///
/// - Parameters:
/// - text: A binding to the text being edited.
/// - field: The field to focus when return is pressed.
/// - focusState: A binding to the focus state.
/// - Returns: A modified view with focus transfer behavior.
func onNewlineFocus<Field: Hashable>(
_ text: Binding<String>,
to field: Field,
on focusState: FocusState<Field?>.Binding
) -> some View {
modifier(
OnNewlineFocusModifier(
text: text,
focusState: focusState,
targetField: field
)
)
}
}
@@ -0,0 +1,43 @@
import SwiftUI
/// A view modifier that resigns the first responder status when the view is tapped.
///
/// This modifier is useful for dismissing the keyboard or other input controls
/// when the user taps on an area of the view that doesn't have interactive content.
struct ResignFocusModifier: ViewModifier {
// MARK: Body
/// The content and behavior of the view modifier.
///
/// Adds a tap gesture that sends the `resignFirstResponder` action
/// to dismiss any active first responder (such as a text field).
func body(content: Content) -> some View {
content
.onTapGesture {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
}
}
// MARK: - View Extension
extension View {
/// Adds a tap gesture that resigns the first responder when the view is tapped.
///
/// This is useful for dismissing the keyboard or other input controls
/// when the user taps outside of an interactive element.
///
/// - Returns: A view that resigns first responder status on tap.
func onResignFocus() -> some View {
modifier(ResignFocusModifier())
}
}