← All repositories

listless

A simple list app for Apple platforms

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


commit a6910ea1226dc5d075217897ba93a47e6b2576b4
parent db6c38cd1b8a5b2eceb3c91f4d1c9e4324b6f218
Author: Michael Camilleri <[email protected]>
Date:   Sun,  8 Mar 2026 14:28:21 +0900

Support flick-to-create gesture

It is a primary goal of Listless to allow a user to quickly enter tasks.
To this end, flicking down quickly should be recognised as evidencing an
intention to create a task even if the distance the screen is pulled is
less than the threshold for a shorter pull. This commit aims to do that.

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

Diffstat

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

2 files changed, 19 insertions(+), 3 deletions(-)

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

@@ -14,6 +14,8 @@ extension TaskListView {
         var pendingTaskID: UUID?
         var isScrollInteracting: Bool = false
 
+        private var pullStartTime: CFTimeInterval = 0
+
         var shouldShowIndicator: Bool {
             indicatorOffset > 0 || isInsertionPending
         }
@@ -32,12 +34,20 @@ extension TaskListView {
         mutating func handlePhaseChange(
             from oldPhase: ScrollPhase,
             to newPhase: ScrollPhase,
-            threshold: CGFloat
+            pullThreshold: CGFloat,
+            flickThreshold: CGFloat
         ) -> Action {
+            if newPhase == .interacting, oldPhase != .interacting {
+                pullStartTime = CACurrentMediaTime()
+            }
             isScrollInteracting = (newPhase == .interacting)
             guard oldPhase == .interacting, newPhase != .interacting else { return .none }
 
-            if pullOffset >= threshold {
+            let elapsed = CACurrentMediaTime() - pullStartTime
+            let isFlick = pullOffset > 0 && elapsed > 0
+                && (pullOffset / elapsed) >= flickThreshold
+
+            if pullOffset >= pullThreshold || isFlick {
                 isInsertionPending = true
                 pendingTaskID = nil
                 return .createTask
@@ -69,6 +79,7 @@ private struct PullGesturesModifier: ViewModifier {
     let activeTaskIDs: [UUID]
     let hasCompletedTasks: Bool
     let pullCreateThreshold: CGFloat
+    let flickThreshold: CGFloat
     let pullClearThreshold: CGFloat
     let onCreateTaskAtTop: () -> UUID
     let onClearCompleted: () -> Void
@@ -117,7 +128,8 @@ private struct PullGesturesModifier: ViewModifier {
         let action = pullToCreate.handlePhaseChange(
             from: oldPhase,
             to: newPhase,
-            threshold: pullCreateThreshold
+            pullThreshold: pullCreateThreshold,
+            flickThreshold: flickThreshold
         )
 
         guard oldPhase == .interacting, newPhase != .interacting else { return }
@@ -178,6 +190,7 @@ extension View {
         activeTaskIDs: [UUID],
         hasCompletedTasks: Bool,
         pullCreateThreshold: CGFloat,
+        flickThreshold: CGFloat,
         pullClearThreshold: CGFloat,
         onCreateTaskAtTop: @escaping () -> UUID,
         onClearCompleted: @escaping () -> Void
@@ -190,6 +203,7 @@ extension View {
                 activeTaskIDs: activeTaskIDs,
                 hasCompletedTasks: hasCompletedTasks,
                 pullCreateThreshold: pullCreateThreshold,
+                flickThreshold: flickThreshold,
                 pullClearThreshold: pullClearThreshold,
                 onCreateTaskAtTop: onCreateTaskAtTop,
                 onClearCompleted: onClearCompleted

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

@@ -174,6 +174,7 @@ struct TaskListView: View, TaskListViewProtocol {
 
     var vStackSpacing: CGFloat { 12 }
     var pullCreateThreshold: CGFloat { 70 }
+    var flickThreshold: CGFloat { 800 }
     var isCompletelyEmpty: Bool { activeTasks.isEmpty && completedTasks.isEmpty }
 
     init(store: TaskStore, syncMonitor: CloudKitSyncMonitor) {
@@ -399,6 +400,7 @@ struct TaskListView: View, TaskListViewProtocol {
                 activeTaskIDs: activeTasks.map(\.id),
                 hasCompletedTasks: !completedTasks.isEmpty,
             pullCreateThreshold: pullCreateThreshold,
+            flickThreshold: flickThreshold,
             pullClearThreshold: pullClearThreshold,
             onCreateTaskAtTop: { createNewTaskAtTop() },
             onClearCompleted: {