Updated the TaskListItem view in the app target to add text field focus support.
This commit is contained in:
@@ -18,6 +18,10 @@ extension TaskListView {
|
|||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
|
/// Tracks which text field currently has keyboard focus.
|
||||||
|
@FocusState
|
||||||
|
private var focusOnField: FieldFocus?
|
||||||
|
|
||||||
/// The todo task to display.
|
/// The todo task to display.
|
||||||
@Bindable
|
@Bindable
|
||||||
private var item: Todo
|
private var item: Todo
|
||||||
@@ -58,18 +62,20 @@ extension TaskListView {
|
|||||||
) {
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
"view.task-list-item.placeholder.title",
|
"view.task-list-item.placeholder.title",
|
||||||
text: $item.title,
|
text: title,
|
||||||
axis: .vertical
|
axis: .vertical
|
||||||
)
|
)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
.focused($focusOnField, equals: .title)
|
||||||
|
|
||||||
if showNote {
|
if isNoteShown {
|
||||||
TextField(
|
TextField(
|
||||||
"view.task-list-item.placeholder.note",
|
"view.task-list-item.placeholder.note",
|
||||||
text: note,
|
text: note,
|
||||||
axis: .vertical
|
axis: .vertical
|
||||||
)
|
)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.focused($focusOnField, equals: .note)
|
||||||
}
|
}
|
||||||
|
|
||||||
DatePicker(selection: $item.due, displayedComponents: .date) {
|
DatePicker(selection: $item.due, displayedComponents: .date) {
|
||||||
@@ -80,7 +86,7 @@ extension TaskListView {
|
|||||||
}
|
}
|
||||||
.autocapitalization(.sentences)
|
.autocapitalization(.sentences)
|
||||||
.autocorrectionDisabled(false)
|
.autocorrectionDisabled(false)
|
||||||
.foregroundStyle(foregroundColor)
|
.foregroundStyle(foregroundStyleView)
|
||||||
.keyboardType(.asciiCapable)
|
.keyboardType(.asciiCapable)
|
||||||
}
|
}
|
||||||
.animation(
|
.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
|
// MARK: - Helpers
|
||||||
|
|
||||||
private extension TaskListView.Item {
|
private extension TaskListView.Item {
|
||||||
|
|
||||||
// MARK: Bindings
|
// 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> {
|
var isCompleted: Binding<Bool> {
|
||||||
.init {
|
.init {
|
||||||
item.completed != nil
|
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<String> {
|
var note: Binding<String> {
|
||||||
.init {
|
.init {
|
||||||
item.note ?? ""
|
item.note ?? ""
|
||||||
} set: {
|
} 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<String> {
|
||||||
|
.init {
|
||||||
|
item.title
|
||||||
|
} set: {
|
||||||
|
setTitle(with: $0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Computed
|
// 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
|
item.completed == nil ? .primary : .secondary
|
||||||
}
|
}
|
||||||
|
|
||||||
var showNote: Bool {
|
/// Determines whether the note field should be visible.
|
||||||
guard let note = item.note else {
|
///
|
||||||
return false
|
/// 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)
|
.listStyle(.grouped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Character+Constants
|
||||||
|
|
||||||
|
private extension Character {
|
||||||
|
static let newLine: Self = "\n"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user