listless

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

commit 9a50bb1fd589366b02341e52bef7c1b29ff46c31
parent 6b47c9892ef2fbd01bb52c7208251a1b7558b3e8
Author: Michael Camilleri <[email protected]>
Date:   Wed, 18 Feb 2026 08:17:58 +0900

Use SwiftUI for more iOS gestures

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

Diffstat:
MListlessiOS/Helpers/TaskRowSwipeGesture.swift | 92+++++++++++++------------------------------------------------------------------
1 file changed, 15 insertions(+), 77 deletions(-)

diff --git a/ListlessiOS/Helpers/TaskRowSwipeGesture.swift b/ListlessiOS/Helpers/TaskRowSwipeGesture.swift @@ -1,5 +1,4 @@ import SwiftUI -import UIKit extension View { func taskSwipeGesture( @@ -33,6 +32,8 @@ struct TaskRowSwipeGesture: ViewModifier { let onComplete: () -> Void let onDelete: () -> Void + @State private var hapticTrigger = false + enum SwipeDirection: Equatable { case left case right @@ -57,21 +58,21 @@ struct TaskRowSwipeGesture: ViewModifier { .animation(.spring(response: 0.3, dampingFraction: 0.8), value: swipeOffset) .contentShape(Rectangle()) } - .gesture( - SwipePanGesture( - isEnabled: !isDragging, - onChanged: { translation in + .simultaneousGesture( + DragGesture(minimumDistance: 10, coordinateSpace: .local) + .onChanged { value in guard !isDragging else { return } handleDragChanged( - horizontalTranslation: translation.x, - verticalTranslation: abs(translation.y) + horizontalTranslation: value.translation.width, + verticalTranslation: abs(value.translation.height) ) - }, - onEnded: { - handleDragEnded() } - ) + .onEnded { _ in + handleDragEnded() + }, + including: isDragging ? .none : .all ) + .sensoryFeedback(.impact(weight: .medium), trigger: hapticTrigger) .onDisappear { resetSwipeState() } @@ -150,17 +151,10 @@ struct TaskRowSwipeGesture: ViewModifier { } } - private func triggerAction(action: @escaping () -> Void) { + private func triggerAction(action: () -> Void) { isTriggered = true - - // Trigger haptic feedback - let generator = UIImpactFeedbackGenerator(style: .medium) - generator.impactOccurred() - - // Execute action after a brief delay to show visual feedback - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - action() - } + hapticTrigger.toggle() + action() } private func resetSwipeState() { @@ -174,59 +168,3 @@ struct TaskRowSwipeGesture: ViewModifier { return min(abs(offset) / threshold, 1.0) } } - -// MARK: - UIKit Pan Gesture via UIGestureRecognizerRepresentable - -/// A UIPanGestureRecognizer bridged into SwiftUI. Each row gets its own -/// recognizer; SwiftUI manages the lifecycle automatically — no manual -/// UIView host-finding or marker-based hit-testing needed. -private struct SwipePanGesture: UIGestureRecognizerRepresentable { - let isEnabled: Bool - let onChanged: (CGPoint) -> Void - let onEnded: () -> Void - - func makeUIGestureRecognizer(context: Context) -> UIPanGestureRecognizer { - let pan = UIPanGestureRecognizer() - pan.cancelsTouchesInView = false - pan.delaysTouchesBegan = false - pan.maximumNumberOfTouches = 1 - pan.delegate = context.coordinator - return pan - } - - func updateUIGestureRecognizer(_ recognizer: UIPanGestureRecognizer, context: Context) { - context.coordinator.isEnabled = isEnabled - } - - func handleUIGestureRecognizerAction( - _ recognizer: UIPanGestureRecognizer, context: Context - ) { - switch recognizer.state { - case .began, .changed: - onChanged(recognizer.translation(in: recognizer.view)) - case .ended, .cancelled, .failed: - onEnded() - default: - break - } - } - - func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { - Coordinator() - } - - final class Coordinator: NSObject, UIGestureRecognizerDelegate { - var isEnabled: Bool = true - - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - isEnabled - } - - func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - true - } - } -}