listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

commit 3202f4afb9f89a7b1a35add831907644cce2c07e
parent d5b614167ab018f1edee9fa117961f0bca1f1307
Author: Michael Camilleri <[email protected]>
Date:   Fri,  6 Mar 2026 12:15:15 +0900

Fix focus bug in iOS version

Diffstat:
MAGENTS.md | 1+
MListlessiOS/Extensions/TaskListView+NavigationHeader.swift | 2+-
2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -112,6 +112,7 @@ Listless is a to-do list app for Apple platforms. It is intended to run on iPhon - **TaskListView architecture**: Declared separately per platform (`ListlessiOS/Views/TaskListView.swift`, `ListlessMac/Views/TaskListView.swift`), both conforming to `TaskListViewProtocol`. State is grouped by concern: `fState` (focus), `iState` (interaction), `tState` (task/view-local). Shared logic lives in `TaskListView+Logic.swift` as an extension on `TaskListViewProtocol`. Because `private` is file-scoped, stored properties accessed from extensions must be `internal`. Platform-specific extensions that would return `EmptyView()` on the other platform are simply omitted. - **Selection pattern**: Parent owns `@State var selectedTaskID`; children receive `isSelected: Bool` + `onSelect: () -> Void` callback (avoids SwiftUI ForEach update issues with @Binding). - **Focus management**: Single `@FocusState` enum (`FocusField` in `TaskListTypes.swift`) with `.task(UUID)` and `.scrollView` cases. Never use multiple @FocusState variables for related focus. Keyboard handlers return `.ignored` when wrong focus state. + - **iOS focus cleanup**: On iOS, always dismiss focus by setting `focusedField = nil`, never directly to `.scrollView`. The `onChange(of: focusedFieldBinding)` handler intercepts `nil` to run cleanup (e.g. `deleteIfEmpty` for empty tasks) before redirecting to `.scrollView`. Skipping `nil` desyncs SwiftUI focus state from UIKit first responder and can cause crashes. - **Pending focus**: Set both `pendingFocus` and `focusedField` in `createNewTask()`. Do NOT resolve in `.onAppear` (race conditions). Clear `pendingFocus` in `startEditing()`. Guard `deleteIfEmpty()` against `pendingFocus` matches. - **Text editing**: iOS uses `TappableTextField` (UIViewRepresentable wrapping `UITextView`); macOS uses `ClickableTextField` (NSViewRepresentable wrapping `NSTextField`). Both use delegate/coordinator patterns to bridge to SwiftUI. Key gotcha: `onEditingChanged` callbacks from UIKit may arrive during a SwiftUI update pass — defer via `DispatchQueue.main.async`. - **Drag-and-drop**: Both platforms maintain `visualOrder` during drag and commit via `store.moveTask()` on release. macOS uses `onDrag` + `dropDestination`; iOS uses `LongPressGesture.sequenced(before: DragGesture)` via `.simultaneousGesture()`. diff --git a/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift b/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift @@ -34,7 +34,7 @@ extension TaskListView { .contentShape(Rectangle()) .onTapGesture { selectedTaskID = nil - focusedField = .scrollView + focusedField = nil } } }