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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user