From c9a577c487b1091aa9dce786141e5f0eccb18358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Fri, 27 Mar 2026 10:37:56 +0100 Subject: [PATCH 1/3] fix? --- .../apple/RNGestureHandlerButton.mm | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm index cb465e1045..d30e4e98ed 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm @@ -290,6 +290,37 @@ - (void)animateTarget:(RNGHUIView *)target #endif } +#if !TARGET_OS_OSX + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + UITouch *touch = [touches anyObject]; + if (touch.view != self) { + [self sendActionsForControlEvents:UIControlEventTouchDown]; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + UITouch *touch = [touches anyObject]; + if (touch.view != self) { + [self sendActionsForControlEvents:UIControlEventTouchUpInside]; + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + UITouch *touch = [touches anyObject]; + if (touch.view != self) { + [self sendActionsForControlEvents:UIControlEventTouchCancel]; + } +} + +#endif + - (void)handleAnimatePressIn { if (_pendingPressOutBlock) { From 81d25547b0409f718cfa31fce111f926a069caa6 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 3 Apr 2026 14:54:24 +0200 Subject: [PATCH 2/3] Hit test virtual detectors in button --- .../apple/RNGestureHandler.h | 1 + .../apple/RNGestureHandler.mm | 26 +++++ .../apple/RNGestureHandlerButton.mm | 97 ++++++++----------- 3 files changed, 67 insertions(+), 57 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index f4de60db5f..8598a65fe6 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -106,6 +106,7 @@ inState:(RNGestureHandlerState)state fromManualStateChange:(BOOL)fromManualStateChange; - (BOOL)containsPointInView; +- (BOOL)wantsToHandleEventsAtPoint:(CGPoint)point; - (RNGestureHandlerState)state; - (nullable RNGestureHandlerEventExtraData *)eventExtraData:(nonnull id)recognizer; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index 361f51817d..080766418f 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -744,6 +744,32 @@ - (BOOL)containsPointInView return CGRectContainsPoint(hitFrame, location); } +- (BOOL)wantsToHandleEventsAtPoint:(CGPoint)point +{ + RNGHUIView *viewToHitTest = _recognizer.view; + + if ([self usesNativeOrVirtualDetector] && [_recognizer.view.subviews count] > 0) { + viewToHitTest = _recognizer.view.subviews[0]; + } + + if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) { + // In this case, logic detector is attached to the DetectorView, which has a single subview representing + // the actual target view in the RN hierarchy + if ([viewToHitTest respondsToSelector:@selector(touchEventEmitterAtPoint:)]) { + // If the view has touchEventEmitterAtPoint: method, it can be used to determine the viewtag + // of the view under the touch point + facebook::react::SharedTouchEventEmitter eventEmitter = + [(id)viewToHitTest touchEventEmitterAtPoint:point]; + auto viewUnderTouch = eventEmitter->getEventTarget()->getTag(); + + return viewUnderTouch == [_virtualViewTag intValue]; + } + } + + CGRect hitFrame = RNGHHitSlopInsetRect(viewToHitTest.bounds, _hitSlop); + return CGRectContainsPoint(hitFrame, point); +} + - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ([_handlersToWaitFor count]) { diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm index d30e4e98ed..5156bbe53b 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm @@ -166,30 +166,6 @@ - (void)layout [self applyUnderlayCornerRadii]; } -- (BOOL)shouldHandleTouch:(RNGHUIView *)view -{ - if ([view isKindOfClass:[RNGestureHandlerButton class]]) { - RNGestureHandlerButton *button = (RNGestureHandlerButton *)view; - return button.userEnabled; - } - - // Certain subviews such as RCTViewComponentView have been observed to have disabled - // accessibility gesture recognizers such as _UIAccessibilityHUDGateGestureRecognizer, - // ostensibly set by iOS. Such gesture recognizers cause this function to return YES - // even when the passed view is static text and does not respond to touches. This in - // turn prevents the button from receiving touches, breaking functionality. To handle - // such case, we can count only the enabled gesture recognizers when determining - // whether a view should receive touches. - NSPredicate *isEnabledPredicate = [NSPredicate predicateWithFormat:@"isEnabled == YES"]; - NSArray *enabledGestureRecognizers = [view.gestureRecognizers filteredArrayUsingPredicate:isEnabledPredicate]; - -#if !TARGET_OS_OSX - return [view isKindOfClass:[UIControl class]] || [enabledGestureRecognizers count] > 0; -#else - return [view isKindOfClass:[NSControl class]] || [enabledGestureRecognizers count] > 0; -#endif -} - - (void)animateUnderlayToOpacity:(float)toOpacity duration:(NSTimeInterval)durationMs { _underlayLayer.opacity = @@ -290,37 +266,6 @@ - (void)animateTarget:(RNGHUIView *)target #endif } -#if !TARGET_OS_OSX - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesBegan:touches withEvent:event]; - UITouch *touch = [touches anyObject]; - if (touch.view != self) { - [self sendActionsForControlEvents:UIControlEventTouchDown]; - } -} - -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesEnded:touches withEvent:event]; - UITouch *touch = [touches anyObject]; - if (touch.view != self) { - [self sendActionsForControlEvents:UIControlEventTouchUpInside]; - } -} - -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesCancelled:touches withEvent:event]; - UITouch *touch = [touches anyObject]; - if (touch.view != self) { - [self sendActionsForControlEvents:UIControlEventTouchCancel]; - } -} - -#endif - - (void)handleAnimatePressIn { if (_pendingPressOutBlock) { @@ -698,6 +643,44 @@ - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event _isTouchInsideBounds = NO; } +- (BOOL)shouldHandleTouch:(RNGHUIView *)view atPoint:(CGPoint)point +{ + if ([view isKindOfClass:[RNGestureHandlerButton class]]) { + RNGestureHandlerButton *button = (RNGestureHandlerButton *)view; + return button.userEnabled; + } + + // Certain subviews such as RCTViewComponentView have been observed to have disabled + // accessibility gesture recognizers such as _UIAccessibilityHUDGateGestureRecognizer, + // ostensibly set by iOS. Such gesture recognizers cause this function to return YES + // even when the passed view is static text and does not respond to touches. This in + // turn prevents the button from receiving touches, breaking functionality. To handle + // such case, we can count only the enabled gesture recognizers when determining + // whether a view should receive touches. + NSPredicate *isEnabledPredicate = [NSPredicate predicateWithFormat:@"isEnabled == YES"]; + NSArray *enabledGestureRecognizers = [view.gestureRecognizers filteredArrayUsingPredicate:isEnabledPredicate]; + + BOOL gestureRecognizerWantsEvent = NO; + for (UIGestureRecognizer *recognizer in enabledGestureRecognizers) { + RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:recognizer]; + if (handler != nil) { + CGPoint pointInView = [self convertPoint:point toView:view]; + gestureRecognizerWantsEvent = [handler wantsToHandleEventsAtPoint:pointInView]; + } else { + gestureRecognizerWantsEvent = YES; + } + if (gestureRecognizerWantsEvent) { + break; + } + } + +#if !TARGET_OS_OSX + return [view isKindOfClass:[UIControl class]] || gestureRecognizerWantsEvent; +#else + return [view isKindOfClass:[NSControl class]] || [enabledGestureRecognizers count] > 0; +#endif +} + - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { RNGestureHandlerPointerEvents pointerEvents = _pointerEvents; @@ -711,7 +694,7 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event if (!subview.isHidden && subview.alpha > 0) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitView = [subview hitTest:convertedPoint withEvent:event]; - if (hitView != nil && [self shouldHandleTouch:hitView]) { + if (hitView != nil && [self shouldHandleTouch:hitView atPoint:point]) { return hitView; } } @@ -724,7 +707,7 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event } RNGHUIView *inner = [super hitTest:point withEvent:event]; - while (inner && ![self shouldHandleTouch:inner]) { + while (inner && ![self shouldHandleTouch:inner atPoint:point]) { inner = inner.superview; } return inner; From f7c728c40208fe4d1f0694836aeeac9548cde0b6 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 3 Apr 2026 15:03:16 +0200 Subject: [PATCH 3/3] Convert point coordinate space --- packages/react-native-gesture-handler/apple/RNGestureHandler.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index 080766418f..4987126829 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -750,6 +750,7 @@ - (BOOL)wantsToHandleEventsAtPoint:(CGPoint)point if ([self usesNativeOrVirtualDetector] && [_recognizer.view.subviews count] > 0) { viewToHitTest = _recognizer.view.subviews[0]; + point = [_recognizer.view convertPoint:point toView:viewToHitTest]; } if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) {