← All repositories

listless

A simple list app for Apple platforms

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


commit 3a7c2c8ce7c6919f291ef99732ef9c7a79044a83
parent 0b9be197dd29af886fcfc2131d89f3044f3270e5
Author: Michael Camilleri <[email protected]>
Date:   Fri, 27 Feb 2026 22:55:40 +0900

Improve pull-to-clear gesture to use drag distance for threshhold

Co-Authored-By: Codex GPT 5.3 <[email protected]>

Diffstat

MListlessiOS/Extensions/TaskListView+PullGestures.swift | 45+++++++++++++++++++++++++++++++++++----------
MListlessiOS/Views/PullToClear.swift | 2+-

2 files changed, 36 insertions(+), 11 deletions(-)

diff --git a/ListlessiOS/Extensions/TaskListView+PullGestures.swift b/ListlessiOS/Extensions/TaskListView+PullGestures.swift

@@ -62,6 +62,9 @@ private struct PullCreationGestureModifier: ViewModifier {
     @Binding var pullToCreate: TaskListView.PullToCreateState
     @Binding var pullUpOffset: CGFloat
 
+    @State private var isAtBottom = false
+    @State private var clearPullStartedAtBottom = false
+
     let activeTaskIDs: [UUID]
     let hasCompletedTasks: Bool
     let pullCreateThreshold: CGFloat
@@ -76,17 +79,16 @@ private struct PullCreationGestureModifier: ViewModifier {
             } action: { _, pullDistance in
                 pullToCreate.updatePullDistance(pullDistance)
             }
-            .onScrollGeometryChange(for: CGFloat.self) { geo in
-                // Subtract the 20pt bottom content margin (set on ScrollView in TaskListView)
-                // so it doesn't create a dead zone before overscroll registers.
+            .onScrollGeometryChange(for: Bool.self) { geo in
+                // Match the same bottom inset adjustment used by the ScrollView.
                 let adjustedBottomInset = geo.contentInsets.bottom - 20
                 let maxOffset = max(
                     -geo.contentInsets.top,
                     geo.contentSize.height - geo.bounds.size.height + adjustedBottomInset
                 )
-                return max(0, geo.contentOffset.y - maxOffset)
-            } action: { _, pullDistance in
-                pullUpOffset = pullDistance
+                return geo.contentOffset.y >= (maxOffset - 1)
+            } action: { _, atBottom in
+                isAtBottom = atBottom
             }
             .onScrollPhaseChange { oldPhase, newPhase in
                 handlePullToCreateScrollPhaseChange(from: oldPhase, to: newPhase)
@@ -107,6 +109,7 @@ private struct PullCreationGestureModifier: ViewModifier {
             .sensoryFeedback(.impact(weight: .medium), trigger: pullUpOffset >= pullClearThreshold) { old, new in
                 !old && new
             }
+            .simultaneousGesture(clearCompletedPullGesture)
     }
 
     private func handlePullToCreateScrollPhaseChange(from oldPhase: ScrollPhase, to newPhase: ScrollPhase) {
@@ -137,10 +140,32 @@ private struct PullCreationGestureModifier: ViewModifier {
             break
         }
 
-        if pullUpOffset >= pullClearThreshold && hasCompletedTasks {
-            onClearCompleted()
-        }
-        pullUpOffset = 0
+    }
+
+    private var clearCompletedPullGesture: some Gesture {
+        DragGesture(minimumDistance: 0, coordinateSpace: .local)
+            .onChanged { value in
+                guard hasCompletedTasks else { return }
+
+                if !clearPullStartedAtBottom {
+                    clearPullStartedAtBottom = isAtBottom
+                }
+                guard clearPullStartedAtBottom else { return }
+
+                // Use finger travel, not ScrollView rubber-band displacement.
+                pullUpOffset = max(0, -value.translation.height)
+            }
+            .onEnded { _ in
+                defer {
+                    clearPullStartedAtBottom = false
+                    pullUpOffset = 0
+                }
+
+                guard hasCompletedTasks, clearPullStartedAtBottom else { return }
+                if pullUpOffset >= pullClearThreshold {
+                    onClearCompleted()
+                }
+            }
     }
 }
 

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

@@ -1,7 +1,7 @@
 import SwiftUI
 
 /// Pull distance at which the indicator signals readiness and completed task clearing triggers.
-let pullClearThreshold: CGFloat = 70
+let pullClearThreshold: CGFloat = 90
 
 struct PullToClearIndicator: View {
     let pullOffset: CGFloat