diff --git a/App/Sources/Views/TaskList/TaskListItem.swift b/App/Sources/Views/TaskList/TaskListItem.swift index 7a825ba..a2fbfdc 100644 --- a/App/Sources/Views/TaskList/TaskListItem.swift +++ b/App/Sources/Views/TaskList/TaskListItem.swift @@ -18,6 +18,10 @@ extension TaskListView { // MARK: Properties + /// Tracks which text field currently has keyboard focus. + @FocusState + private var focusOnField: FieldFocus? + /// The todo task to display. @Bindable private var item: Todo @@ -58,18 +62,20 @@ extension TaskListView { ) { TextField( "view.task-list-item.placeholder.title", - text: $item.title, + text: title, axis: .vertical ) .font(.headline) + .focused($focusOnField, equals: .title) - if showNote { + if isNoteShown { TextField( "view.task-list-item.placeholder.note", text: note, axis: .vertical ) .font(.subheadline) + .focused($focusOnField, equals: .note) } DatePicker(selection: $item.due, displayedComponents: .date) { @@ -80,7 +86,7 @@ extension TaskListView { } .autocapitalization(.sentences) .autocorrectionDisabled(false) - .foregroundStyle(foregroundColor) + .foregroundStyle(foregroundStyleView) .keyboardType(.asciiCapable) } .animation( @@ -92,12 +98,31 @@ extension TaskListView { } +// MARK: - Enumerations + +private extension TaskListView.Item { + + /// Identifies which text field should receive keyboard focus. + enum FieldFocus { + /// The note text field has focus. + case note + /// The title text field has focus. + case title + } + +} + // MARK: - Helpers private extension TaskListView.Item { // MARK: Bindings + /// A binding to the task's completion state. + /// + /// This computed binding converts between the optional `completed` date and a boolean + /// value for the completion toggle. When set to `true`, it marks the task as completed + /// with the current date; when set to `false`, it clears the completion date. var isCompleted: Binding { .init { item.completed != nil @@ -106,26 +131,107 @@ private extension TaskListView.Item { } } + /// A binding to the note text of a todo task. + /// + /// This computed binding provides a non-optional string binding for the note field, + /// converting `nil` values to empty strings for seamless text field interaction. var note: Binding { .init { item.note ?? "" } set: { - item.note = $0.isEmpty ? nil : $0 + setNote(with: $0) + } + } + + /// A binding to the title text of a todo task. + /// + /// This computed binding provides access to the title of a todo task with special handling + /// for newline characters. + var title: Binding { + .init { + item.title + } set: { + setTitle(with: $0) } } // MARK: Computed - var foregroundColor: Color { + /// The foreground color style for the components in this view. + /// + /// Returns `.primary` for incomplete tasks and `.secondary` for completed tasks, + /// providing visual feedback about the task's completion state by dimming + /// completed items. + var foregroundStyleView: Color { item.completed == nil ? .primary : .secondary } - var showNote: Bool { - guard let note = item.note else { - return false + /// Determines whether the note field should be visible. + /// + /// The note field is shown when: + /// - Any text field has focus (allowing users to add a note even if one doesn't exist yet) + /// - The todo task has a note with non-whitespace content + var isNoteShown: Bool { + guard focusOnField != nil else { + guard let note = item.note else { + return false + } + + return note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false } - return note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false + return true + } + + // MARK: Functions + + /// Updates the note of a todo task with special handling for newlines and empty content. + /// + /// This method provides intelligent note field behavior: + /// - Empty text immediately clears the note + /// - Newlines at the end of the text trigger special handling: + /// - If the text is only whitespace/newlines, the note is cleared and focus is removed + /// - Otherwise, the text (including newlines) is saved normally + /// - All other text is saved as-is + /// + /// This allows users to press return on an empty note field to dismiss it, + /// while still supporting multi-line notes. + /// + /// - Parameter text: The new note text to set. + func setNote(with text: String) { + guard text.isEmpty == false else { + item.note = text + return + } + + if text.last == .newLine { + guard text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true else { + item.note = text + return + } + + item.note = nil + focusOnField = nil + } + else { + item.note = text + } + } + + /// Updates the title of a todo task with special handling for newline characters. + /// + /// When the user presses return in the title field (adding a newline), this function + /// prevents the newline from being added to the title and instead moves keyboard + /// focus to the note field. If no newline is present, the title is updated normally. + /// + /// - Parameter text: The new title text to set. + func setTitle(with text: String) { + guard text.lastIndex(of: .newLine) == nil else { + focusOnField = .note + return + } + + item.title = text } } @@ -146,3 +252,9 @@ private extension TaskListView.Item { } .listStyle(.grouped) } + +// MARK: - Character+Constants + +private extension Character { + static let newLine: Self = "\n" +}