ItemRowDragGesture.swift (3234B)
1 import SwiftUI 2 3 extension 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 22 struct 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. 48 private 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 }