From 19f25307bd3e46f69b7766e8ce05576088e163df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 6 Aug 2026 11:44:46 +0200 Subject: [PATCH 1/3] Fling touch events --- .../apple/Handlers/RNFlingHandler.m | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m index f526da7810..297efb98c6 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m @@ -143,13 +143,29 @@ - (void)handleGesture:(NSPanGestureRecognizer *)gestureRecognizer [_gestureHandler handleGesture:self]; } +- (void)triggerAction +{ + [_gestureHandler handleGesture:self fromReset:NO fromManualStateChange:NO]; +} + +- (void)triggerActionFromReset +{ + [_gestureHandler handleGesture:self fromReset:YES fromManualStateChange:NO]; +} + - (void)mouseDown:(NSEvent *)event { + [_gestureHandler setCurrentPointerTypeToMouse]; + [_gestureHandler reset]; [super mouseDown:event]; startPosition = [self locationInView:self.view]; startTime = CACurrentMediaTime(); + [_gestureHandler.pointerTracker touchesBegan:[NSSet setWithObject:event] withEvent:event]; + // Send the BEGAN event, mirroring what the iOS recognizer does in touchesBegan. + [self triggerAction]; + self.state = NSGestureRecognizerStatePossible; __weak typeof(self) weakSelf = self; @@ -172,6 +188,8 @@ - (void)mouseDragged:(NSEvent *)event { [super mouseDragged:event]; + [_gestureHandler.pointerTracker touchesMoved:[NSSet setWithObject:event] withEvent:event]; + NSPoint currentPosition = [self locationInView:self.view]; double currentTime = CACurrentMediaTime(); @@ -191,12 +209,22 @@ - (void)mouseUp:(NSEvent *)event { [super mouseUp:event]; + [_gestureHandler.pointerTracker touchesEnded:[NSSet setWithObject:event] withEvent:event]; + dispatch_block_cancel(failFlingAction); self.state = self.state == NSGestureRecognizerStateChanged ? NSGestureRecognizerStateEnded : NSGestureRecognizerStateFailed; } +- (void)reset +{ + [self triggerActionFromReset]; + [_gestureHandler.pointerTracker reset]; + [super reset]; + [_gestureHandler reset]; +} + - (void)tryActivate:(RNGHVector *)velocityVector { bool isAligned = NO; From df3e590dc69cde0fa56fb08cb21217af09b6b898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 6 Aug 2026 12:28:03 +0200 Subject: [PATCH 2/3] Cancel on reset --- .../apple/Handlers/RNFlingHandler.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m index 297efb98c6..5b27be0d5f 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m @@ -205,13 +205,21 @@ - (void)mouseDragged:(NSEvent *)event [self tryActivate:velocityVector]; } +- (void)cancelFailFlingAction +{ + if (failFlingAction != nil) { + dispatch_block_cancel(failFlingAction); + failFlingAction = nil; + } +} + - (void)mouseUp:(NSEvent *)event { [super mouseUp:event]; [_gestureHandler.pointerTracker touchesEnded:[NSSet setWithObject:event] withEvent:event]; - dispatch_block_cancel(failFlingAction); + [self cancelFailFlingAction]; self.state = self.state == NSGestureRecognizerStateChanged ? NSGestureRecognizerStateEnded : NSGestureRecognizerStateFailed; @@ -221,6 +229,9 @@ - (void)reset { [self triggerActionFromReset]; [_gestureHandler.pointerTracker reset]; + // The gesture may end without a mouse-up (view unmount, cancellation) — a still-scheduled + // fail block would fire later and set the state to Failed during the next gesture. + [self cancelFailFlingAction]; [super reset]; [_gestureHandler reset]; } From d77582471763051559a72b6dd5bd3515013bb45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 7 Aug 2026 11:42:16 +0200 Subject: [PATCH 3/3] Rename --- .../apple/Handlers/RNFlingHandler.m | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m index 5b27be0d5f..dee0bb590d 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m +++ b/packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m @@ -101,7 +101,8 @@ - (CGPoint)getLastLocation #else @interface RNBetterSwipeGestureRecognizer : NSGestureRecognizer { - dispatch_block_t failFlingAction; + // scheduled on mouseDown; fails the gesture if the fling doesn't complete within maxDuration + dispatch_block_t maxDurationTimeout; int maxDuration; int minVelocity; double defaultAlignmentCone; @@ -170,7 +171,7 @@ - (void)mouseDown:(NSEvent *)event __weak typeof(self) weakSelf = self; - failFlingAction = dispatch_block_create(0, ^{ + maxDurationTimeout = dispatch_block_create(0, ^{ __strong typeof(self) strongSelf = weakSelf; if (strongSelf) { @@ -181,7 +182,7 @@ - (void)mouseDown:(NSEvent *)event dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(maxDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), - failFlingAction); + maxDurationTimeout); } - (void)mouseDragged:(NSEvent *)event @@ -205,11 +206,11 @@ - (void)mouseDragged:(NSEvent *)event [self tryActivate:velocityVector]; } -- (void)cancelFailFlingAction +- (void)cancelMaxDurationTimeout { - if (failFlingAction != nil) { - dispatch_block_cancel(failFlingAction); - failFlingAction = nil; + if (maxDurationTimeout != nil) { + dispatch_block_cancel(maxDurationTimeout); + maxDurationTimeout = nil; } } @@ -219,7 +220,7 @@ - (void)mouseUp:(NSEvent *)event [_gestureHandler.pointerTracker touchesEnded:[NSSet setWithObject:event] withEvent:event]; - [self cancelFailFlingAction]; + [self cancelMaxDurationTimeout]; self.state = self.state == NSGestureRecognizerStateChanged ? NSGestureRecognizerStateEnded : NSGestureRecognizerStateFailed; @@ -230,8 +231,8 @@ - (void)reset [self triggerActionFromReset]; [_gestureHandler.pointerTracker reset]; // The gesture may end without a mouse-up (view unmount, cancellation) — a still-scheduled - // fail block would fire later and set the state to Failed during the next gesture. - [self cancelFailFlingAction]; + // timeout would fire later and set the state to Failed during the next gesture. + [self cancelMaxDurationTimeout]; [super reset]; [_gestureHandler reset]; }