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
+27 -8
View File
@@ -21,6 +21,10 @@ 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?
/// The view model managing the state and logic for this view.
@State
@@ -50,22 +54,36 @@ struct NewTaskView: View {
Section("view.new-task.section.task") {
TextField(
"view.new-task.placeholder.title",
text: $viewModel.title
text: $viewModel.title,
axis: .vertical
)
.focused(
$inputFocus,
equals: .title
)
.onNewlineFocus(
$viewModel.title,
to: .note,
on: $inputFocus
)
.autocapitalization(.sentences)
.autocorrectionDisabled(false)
.keyboardType(.asciiCapable)
TextField(
"view.new-task.placeholder.note",
text: $viewModel.note,
axis: .vertical
)
.autocapitalization(.sentences)
.autocorrectionDisabled(false)
.keyboardType(.asciiCapable)
.lineLimit(3 ... 6)
.focused(
$inputFocus,
equals: .note
)
.onEmptyNewlineResignFocus(
text: $viewModel.note,
from: $inputFocus
)
}
.autocapitalization(.sentences)
.autocorrectionDisabled(false)
.keyboardType(.default)
Section("view.new-task.section.due-date") {
DatePicker(
@@ -78,6 +96,7 @@ struct NewTaskView: View {
}
.navigationTitle("view.new-task.navigation.title")
.navigationBarTitleDisplayMode(.inline)
.onResignFocus()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(role: .cancel) {
@@ -124,6 +124,7 @@ extension TaskListView {
items
}
.listStyle(.grouped)
.onResignFocus()
}
}
}
+60 -10
View File
@@ -18,6 +18,10 @@ extension TaskListView {
// MARK: Properties
/// Tracks which text field currently has keyboard focus.
@FocusState
private var inputFocus: InputFieldFocus?
/// The todo task to display.
@Bindable
private var item: Todo
@@ -31,7 +35,7 @@ extension TaskListView {
///
/// - Parameter item: The todo task to display and edit.
init(_ item: Todo) {
_item = Bindable(item)
self._item = Bindable(item)
}
// MARK: Body
@@ -50,6 +54,9 @@ extension TaskListView {
"view.task-list-item.placeholder.completed",
isOn: isCompleted
)
.sensoryFeedback(trigger: item.completed) {
item.completed != nil ? .increase : .decrease
}
.toggleStyle(.completion)
VStack(
@@ -62,17 +69,37 @@ extension TaskListView {
axis: .vertical
)
.font(.headline)
.focused(
$inputFocus,
equals: .title
)
.onNewlineFocus(
$item.title,
to: .note,
on: $inputFocus
)
if showNote {
if isNoteShown {
TextField(
"view.task-list-item.placeholder.note",
text: note,
axis: .vertical
)
.font(.subheadline)
.focused(
$inputFocus,
equals: .note
)
.onEmptyNewlineResignFocus(
text: note,
from: $inputFocus
)
}
DatePicker(selection: $item.due, displayedComponents: .date) {
DatePicker(
selection: $item.due,
displayedComponents: .date
) {
Image(systemName: "calendar")
}
.frame(width: 160)
@@ -80,8 +107,8 @@ extension TaskListView {
}
.autocapitalization(.sentences)
.autocorrectionDisabled(false)
.foregroundStyle(foregroundColor)
.keyboardType(.asciiCapable)
.foregroundStyle(foregroundStyleView)
.keyboardType(.default)
}
.animation(
.easeInOut,
@@ -98,6 +125,11 @@ 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<Bool> {
.init {
item.completed != nil
@@ -106,6 +138,10 @@ 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<String> {
.init {
item.note ?? ""
@@ -116,16 +152,30 @@ private extension TaskListView.Item {
// 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 inputFocus != 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
}
}