From 7e62fcc7fd1eb841b21783ed821aa93d5450ec3a Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 26 Mar 2026 11:08:06 +0100 Subject: [PATCH 1/2] Ignore apple's "feature" --- .../apple/RNGestureHandlerButton.mm | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm index c433e64bec..953fef5281 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm @@ -41,10 +41,12 @@ */ @implementation RNGestureHandlerButton { CALayer *_underlayLayer; + BOOL _isTouchInsideBounds; } - (void)commonInit { + _isTouchInsideBounds = NO; _hitTestEdgeInsets = UIEdgeInsetsZero; _userEnabled = YES; _pointerEvents = RNGestureHandlerPointerEventsAuto; @@ -275,6 +277,52 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event return CGRectContainsPoint(hitFrame, point); } +- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event +{ + _isTouchInsideBounds = YES; + return [super beginTrackingWithTouch:touch withEvent:event]; +} + +- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event +{ + // DO NOT call super. We are entirely taking over the drag event generation. + + CGPoint location = [touch locationInView:self]; + CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); + BOOL currentlyInside = CGRectContainsPoint(hitFrame, location); + + if (currentlyInside) { + if (!_isTouchInsideBounds) { + [self sendActionsForControlEvents:UIControlEventTouchDragEnter]; + _isTouchInsideBounds = YES; + } + [self sendActionsForControlEvents:UIControlEventTouchDragInside]; + } else { + if (_isTouchInsideBounds) { + [self sendActionsForControlEvents:UIControlEventTouchDragExit]; + _isTouchInsideBounds = NO; + } + [self sendActionsForControlEvents:UIControlEventTouchDragOutside]; + } + + // If `cancelTrackingWithEvent` was called, `self.tracking` will be NO. + return self.tracking; +} + +- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event +{ + // Also bypass super here so that the final "up" event respects the + // strict bounds, rather than Apple's 70-point. + + CGPoint location = [touch locationInView:self]; + CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); + if (CGRectContainsPoint(hitFrame, location)) { + [self sendActionsForControlEvents:UIControlEventTouchUpInside]; + } else { + [self sendActionsForControlEvents:UIControlEventTouchUpOutside]; + } +} + - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { RNGestureHandlerPointerEvents pointerEvents = _pointerEvents; From 08cacf2fc4c6ad43f132d68d4ec78dceabc9efc7 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 26 Mar 2026 11:51:10 +0100 Subject: [PATCH 2/2] Review changes --- .../apple/RNGestureHandlerButton.mm | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm index 953fef5281..ad06c02617 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm @@ -296,13 +296,21 @@ - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event [self sendActionsForControlEvents:UIControlEventTouchDragEnter]; _isTouchInsideBounds = YES; } - [self sendActionsForControlEvents:UIControlEventTouchDragInside]; + + // Targets may call `cancelTrackingWithEvent:` in response to DragEnter. + if (self.tracking) { + [self sendActionsForControlEvents:UIControlEventTouchDragInside]; + } } else { if (_isTouchInsideBounds) { [self sendActionsForControlEvents:UIControlEventTouchDragExit]; _isTouchInsideBounds = NO; } - [self sendActionsForControlEvents:UIControlEventTouchDragOutside]; + + // Targets may call `cancelTrackingWithEvent:` in response to DragExit. + if (self.tracking) { + [self sendActionsForControlEvents:UIControlEventTouchDragOutside]; + } } // If `cancelTrackingWithEvent` was called, `self.tracking` will be NO. @@ -314,13 +322,17 @@ - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event // Also bypass super here so that the final "up" event respects the // strict bounds, rather than Apple's 70-point. - CGPoint location = [touch locationInView:self]; - CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); - if (CGRectContainsPoint(hitFrame, location)) { - [self sendActionsForControlEvents:UIControlEventTouchUpInside]; - } else { - [self sendActionsForControlEvents:UIControlEventTouchUpOutside]; + if (touch != nil) { + CGPoint location = [touch locationInView:self]; + CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); + if (CGRectContainsPoint(hitFrame, location)) { + [self sendActionsForControlEvents:UIControlEventTouchUpInside]; + } else { + [self sendActionsForControlEvents:UIControlEventTouchUpOutside]; + } } + + _isTouchInsideBounds = NO; } - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event