commit 03fc0ea2ba94d05daca8b79e54100f70b8602fb1
parent 22a8bebf4768a804fd312c5459db4607435997d3
Author: Michael Camilleri <[email protected]>
Date: Fri, 20 Mar 2026 17:48:04 +0900
Make scroll/swipe heuristic more robust
This is an attempt to make the decision about whether to recognise a
horizontal swipe gesture when a scroll gesture has begun more robust.
Previously, the heuristic merely looked at whether the scroll phase was
`.idle` or not. This checks the direction of the gesture.
Co-Authored-By: Claude 4.6 Opus <[email protected]>
Diffstat:
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/ListlessiOS/Helpers/TaskRowSwipeGesture.swift b/ListlessiOS/Helpers/TaskRowSwipeGesture.swift
@@ -36,6 +36,7 @@ struct TaskRowSwipeGesture: ViewModifier {
let onDelete: () -> Void
@State private var hapticTrigger = false
+ @State private var activeGestureAxis: ActiveGestureAxis = .undecided
enum SwipeDirection: Equatable {
case left
@@ -43,6 +44,12 @@ struct TaskRowSwipeGesture: ViewModifier {
case none
}
+ private enum ActiveGestureAxis {
+ case undecided
+ case horizontal
+ case vertical
+ }
+
private let completeThreshold: CGFloat = 40
private let deleteThreshold: CGFloat = 80
private let horizontalBufferPt: CGFloat = 10
@@ -63,7 +70,12 @@ struct TaskRowSwipeGesture: ViewModifier {
.applySwipeGesture(
isDragging: isDragging,
onChanged: { translation in
- guard !isDragging, !isScrolling else { return }
+ guard !isDragging else { return }
+ updateActiveGestureAxis(
+ horizontalTranslation: translation.width,
+ verticalTranslation: abs(translation.height)
+ )
+ guard activeGestureAxis == .horizontal else { return }
handleDragChanged(
horizontalTranslation: translation.width,
verticalTranslation: abs(translation.height)
@@ -123,6 +135,8 @@ struct TaskRowSwipeGesture: ViewModifier {
}
private func handleDragEnded() {
+ defer { activeGestureAxis = .undecided }
+
guard !isDragging else {
// A drag-reorder started during or after this swipe — spring back, no action.
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
@@ -162,12 +176,23 @@ struct TaskRowSwipeGesture: ViewModifier {
swipeOffset = 0
swipeDirection = .none
isTriggered = false
+ activeGestureAxis = .undecided
}
private func backgroundOpacity(offset: CGFloat) -> CGFloat {
let threshold = offset >= 0 ? completeThreshold : deleteThreshold
return min(abs(offset) / threshold, 1.0)
}
+
+ private func updateActiveGestureAxis(horizontalTranslation: CGFloat, verticalTranslation: CGFloat) {
+ guard activeGestureAxis == .undecided else { return }
+
+ if abs(horizontalTranslation) > verticalTranslation + horizontalBufferPt {
+ activeGestureAxis = .horizontal
+ } else if verticalTranslation > abs(horizontalTranslation) + horizontalBufferPt {
+ activeGestureAxis = .vertical
+ }
+ }
}
// MARK: - iOS 26 workaround: UIGestureRecognizerRepresentable