← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


commit 169d0e78f11c809764421878723a3b5380e522de
parent e542c7f27df71ce7393f12739caf6f5b050a7563
Author: Michael Camilleri <[email protected]>
Date:   Mon, 23 Mar 2026 16:04:16 +0900

Fix pull-to-create animation

A previous commit attempted to make the pull-to-create animation
smoother by fixing the structure of the view hierarchy. After using the
app on my phone, it became evident that this new structure was broken.
When submitting a new item from a draft row located at the top of the
list, the new list would jump down. This commit restructures the view
hierarchy in a way that eliminates this bug.

Co-Authored-By: Claude 4.6 Opus <[email protected]>

Diffstat

MListlessiOS/Views/TaskListView.swift | 42+++++++++++++++++-------------------------

1 file changed, 17 insertions(+), 25 deletions(-)

diff --git a/ListlessiOS/Views/TaskListView.swift b/ListlessiOS/Views/TaskListView.swift

@@ -239,35 +239,23 @@ struct TaskListView: View, TaskListViewProtocol {
     /// The phantom's UITextView is created while the indicator is visible
     /// (during the pull), so it's ready when the user releases.
     @ViewBuilder var pullToCreateIndicatorRow: some View {
-        let showPhantom = isPrependDraftVisible
         let pullOffset = pState.pullToCreate.pullOffset
         let indicatorHeight = PullToCreateIndicator.indicatorHeight
-        ZStack(alignment: .topLeading) {
-            PullToCreateIndicator(
-                pullOffset: max(
-                    0,
-                    pState.pullToCreate.indicatorDisplayOffset(
-                        threshold: pullCreateThreshold
-                    ) - vStackSpacing
-                ),
-                threshold: max(0, pullCreateThreshold - vStackSpacing),
-                hasRowsBelow: false
-            )
-            .padding(.bottom, -indicatorHeight)
-            .opacity(showPhantom ? 0 : 1)
-
-            draftPrependRow
-                .frame(height: showPhantom ? nil : 0)
-                .opacity(showPhantom ? 1 : 0)
-                // Instant swap — no animation on height or opacity.
-                .animation(nil, value: showPhantom)
-        }
+        PullToCreateIndicator(
+            pullOffset: max(
+                0,
+                pState.pullToCreate.indicatorDisplayOffset(
+                    threshold: pullCreateThreshold
+                ) - vStackSpacing
+            ),
+            threshold: max(0, pullCreateThreshold - vStackSpacing),
+            hasRowsBelow: false
+        )
+        .padding(.bottom, -indicatorHeight)
+        .opacity(isPrependDraftVisible ? 0 : 1)
         .offset(
-            y: showPhantom
-                ? pState.draftSettleOffset
-                : vStackSpacing - min(pullOffset, indicatorHeight + vStackSpacing)
+            y: vStackSpacing - min(pullOffset, indicatorHeight + vStackSpacing)
         )
-        .animation(nil, value: showPhantom)
     }
 
     /// The draft row content styled to match a task row. Controlled by the
@@ -477,6 +465,10 @@ struct TaskListView: View, TaskListViewProtocol {
                         Color.clear.frame(height: pState.headerHeight)
                         pullToCreateIndicatorRow
                     }
+                    if isPrependDraftVisible {
+                        draftPrependRow
+                            .offset(y: pState.draftSettleOffset)
+                    }
                     taskRows
                 }
                 .offset(