← All repositories

listless

A simple list app for Apple platforms

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


ListlessiOS/Helpers/ItemRowDragGesture.swift (3234B)

1import SwiftUI
2
3extension View {
4    func itemDragGesture(
5        isActive: Bool,
6        itemID: UUID,
7        onDragStart: @escaping (CGFloat) -> Void,
8        onDragChanged: @escaping (CGPoint) -> Void,
9        onDragEnded: @escaping () -> Void
10    ) -> some View {
11        self.modifier(
12            ItemRowDragGesture(
13                isActive: isActive,
14                itemID: itemID,
15                onDragStart: onDragStart,
16                onDragChanged: onDragChanged,
17                onDragEnded: onDragEnded
18            ))
19    }
20}
21
22struct ItemRowDragGesture: ViewModifier {
23    let isActive: Bool
24    let itemID: UUID
25    let onDragStart: (CGFloat) -> Void
26    let onDragChanged: (CGPoint) -> Void
27    let onDragEnded: () -> Void
28
29    func body(content: Content) -> some View {
30        content
31            .gesture(
32                SimultaneousDragGesture(
33                    isActive: isActive,
34                    onDragStart: onDragStart,
35                    onDragChanged: onDragChanged,
36                    onDragEnded: onDragEnded
37                )
38            )
39    }
40}
41
42// MARK: - iOS 26 workaround
43
44/// Uses UILongPressGestureRecognizer (minimumPressDuration: 0.4) via
45/// UIGestureRecognizerRepresentable to avoid iOS 26's child-gesture-blocks-
46/// ancestor issue. The delegate returns shouldRecognizeSimultaneouslyWith:true
47/// so the ScrollView's pan gesture is preserved.
48private struct SimultaneousDragGesture: UIGestureRecognizerRepresentable {
49    let isActive: Bool
50    let onDragStart: (CGFloat) -> Void
51    let onDragChanged: (CGPoint) -> Void
52    let onDragEnded: () -> Void
53
54    func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
55        let recognizer = UILongPressGestureRecognizer()
56        recognizer.minimumPressDuration = 0.4
57        recognizer.delegate = context.coordinator
58        recognizer.isEnabled = isActive
59        return recognizer
60    }
61
62    func updateUIGestureRecognizer(_ recognizer: UILongPressGestureRecognizer, context: Context) {
63        recognizer.isEnabled = isActive
64    }
65
66    func handleUIGestureRecognizerAction(
67        _ recognizer: UILongPressGestureRecognizer, context: Context
68    ) {
69        switch recognizer.state {
70        case .began:
71            let width = recognizer.view?.bounds.width ?? 0
72            onDragStart(width)
73
74        case .changed:
75            let location = recognizer.location(in: recognizer.view?.window)
76            onDragChanged(location)
77
78        case .ended, .cancelled:
79            onDragEnded()
80
81        default:
82            break
83        }
84    }
85
86    func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator {
87        Coordinator()
88    }
89
90    class Coordinator: NSObject, UIGestureRecognizerDelegate {
91        func gestureRecognizer(
92            _ gestureRecognizer: UIGestureRecognizer,
93            shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
94        ) -> Bool {
95            true
96        }
97
98        func gestureRecognizer(
99            _ gestureRecognizer: UIGestureRecognizer,
100            shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
101        ) -> Bool {
102            otherGestureRecognizer.view is UITextView
103        }
104    }
105}