commit f7d7a94f5cf39024ae4f5f77e04d5b2d8a5440f3
parent c345c5b50d3817879cf0d6bd0248ad2bd3cfc607
Author: Michael Camilleri <[email protected]>
Date: Sat, 21 Mar 2026 03:22:16 +0900
Align pull-to-clear and pull-to-create gestures
Prior to this commit, the pull-to-clear gesture had been implemented
differently to the visually similar pull-to-create gesture. This commit
aligns them. This has the benefit of preventing a pull-to-clear action
registering while swiping on a row.
Co-Authored-By: Claude 4.6 Opus <[email protected]>
Diffstat:
3 files changed, 23 insertions(+), 38 deletions(-)
diff --git a/ListlessiOS/Extensions/TaskListView+PullGestures.swift b/ListlessiOS/Extensions/TaskListView+PullGestures.swift
@@ -65,10 +65,8 @@ extension TaskListView {
private struct PullGesturesModifier: ViewModifier {
@Binding var pullToCreate: TaskListView.PullToCreateState
@Binding var pullUpOffset: CGFloat
- @Binding var isDragging: Bool
- @State private var isAtBottom = false
- @State private var clearPullStartedAtBottom = false
+ @State private var isScrollInteracting = false
let isDraftOpen: Bool
let hasCompletedTasks: Bool
@@ -85,19 +83,28 @@ private struct PullGesturesModifier: ViewModifier {
} action: { _, pullDistance in
pullToCreate.updatePullDistance(pullDistance)
}
- .onScrollGeometryChange(for: Bool.self) { geo in
- // Match the same bottom inset adjustment used by the ScrollView.
+ .onScrollGeometryChange(for: CGFloat.self) { geo in
let adjustedBottomInset = geo.contentInsets.bottom - 20
let maxOffset = max(
-geo.contentInsets.top,
geo.contentSize.height - geo.bounds.size.height + adjustedBottomInset
)
- return geo.contentOffset.y >= (maxOffset - 1)
- } action: { _, atBottom in
- isAtBottom = atBottom
+ return max(0, geo.contentOffset.y - maxOffset)
+ } action: { _, bottomOverscroll in
+ guard hasCompletedTasks, isScrollInteracting else { return }
+ pullUpOffset = bottomOverscroll
}
.onScrollPhaseChange { oldPhase, newPhase in
+ if newPhase == .interacting {
+ isScrollInteracting = true
+ }
+
handlePullToCreateScrollPhaseChange(from: oldPhase, to: newPhase)
+
+ if oldPhase == .interacting, newPhase != .interacting {
+ handlePullToClearRelease()
+ isScrollInteracting = false
+ }
}
.sensoryFeedback(
.impact(weight: .medium),
@@ -108,7 +115,6 @@ private struct PullGesturesModifier: ViewModifier {
.sensoryFeedback(.impact(weight: .medium), trigger: pullUpOffset >= pullClearThreshold) { old, new in
!old && new
}
- .simultaneousGesture(clearCompletedPullGesture)
}
private func handlePullToCreateScrollPhaseChange(from oldPhase: ScrollPhase, to newPhase: ScrollPhase) {
@@ -136,33 +142,15 @@ private struct PullGesturesModifier: ViewModifier {
case .none:
break
}
-
}
- private var clearCompletedPullGesture: some Gesture {
- DragGesture(minimumDistance: 0, coordinateSpace: .local)
- .onChanged { value in
- guard hasCompletedTasks, !isDragging 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()
- }
- }
+ private func handlePullToClearRelease() {
+ guard hasCompletedTasks, pullUpOffset >= pullClearThreshold else {
+ pullUpOffset = 0
+ return
+ }
+ pullUpOffset = 0
+ onClearCompleted()
}
}
@@ -170,7 +158,6 @@ extension View {
func pullGestures(
pullToCreate: Binding<TaskListView.PullToCreateState>,
pullUpOffset: Binding<CGFloat>,
- isDragging: Binding<Bool>,
isDraftOpen: Bool,
hasCompletedTasks: Bool,
pullCreateThreshold: CGFloat,
@@ -183,7 +170,6 @@ extension View {
PullGesturesModifier(
pullToCreate: pullToCreate,
pullUpOffset: pullUpOffset,
- isDragging: isDragging,
isDraftOpen: isDraftOpen,
hasCompletedTasks: hasCompletedTasks,
pullCreateThreshold: pullCreateThreshold,
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 = 90
+let pullClearThreshold: CGFloat = 50
struct PullToClearIndicator: View {
let pullOffset: CGFloat
diff --git a/ListlessiOS/Views/TaskListView.swift b/ListlessiOS/Views/TaskListView.swift
@@ -556,7 +556,6 @@ struct TaskListView: View, TaskListViewProtocol {
.pullGestures(
pullToCreate: pullToCreateStateBinding,
pullUpOffset: pullUpOffsetStateBinding,
- isDragging: isDraggingStateBinding,
isDraftOpen: draftPlacement != nil,
hasCompletedTasks: !completedTasks.isEmpty,
pullCreateThreshold: pullCreateThreshold,