listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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:
MListlessiOS/Helpers/TaskRowSwipeGesture.swift | 27++++++++++++++++++++++++++-
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