Skip to content

Commit 4768080

Browse files
[ios] fix pressable with intercepting detector (#4041)
## Description On iOS, when under pressable, we had an intercepting detector handling a nested text gesture, pressable did not activate when we pressed on the part of the text with no virtual gesture attached to it. This PR should fix the issue. The problem was that when the button was hit-testing its children, it assumed that the gesture recognizer always wanted to receive events (it was only checking whether there was any). The new implementation goes through the list of found recognizers, checking if they want to handle touch at the specified point. Note: on web the nested text gesture never activates when under a pressable, web needs further care. ## Test plan Tested on the following example <details> ```tsx import { Text } from 'react-native'; import { GestureHandlerRootView, InterceptingGestureDetector, Pressable, VirtualGestureDetector, useTapGesture, } from 'react-native-gesture-handler'; export default function Reproduction() { const innerTap = useTapGesture({ onActivate: () => { 'worklet'; console.log('RNGH: Inner onPress'); }, }); return ( <GestureHandlerRootView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Pressable style={{ width: 100, height: 100, backgroundColor: 'pink' }} onPress={() => console.log('RNGH: Pressable onPress')}> <InterceptingGestureDetector> <Text> Outer{' '} <VirtualGestureDetector gesture={innerTap}> <Text>Inner</Text> </VirtualGestureDetector> </Text> </InterceptingGestureDetector> </Pressable> </GestureHandlerRootView> ); } ``` --------- Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
1 parent d4f7818 commit 4768080

3 files changed

Lines changed: 68 additions & 26 deletions

File tree

packages/react-native-gesture-handler/apple/RNGestureHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
inState:(RNGestureHandlerState)state
107107
fromManualStateChange:(BOOL)fromManualStateChange;
108108
- (BOOL)containsPointInView;
109+
- (BOOL)wantsToHandleEventsAtPoint:(CGPoint)point;
109110
- (RNGestureHandlerState)state;
110111
- (nullable RNGestureHandlerEventExtraData *)eventExtraData:(nonnull id)recognizer;
111112

packages/react-native-gesture-handler/apple/RNGestureHandler.mm

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,33 @@ - (BOOL)containsPointInView
744744
return CGRectContainsPoint(hitFrame, location);
745745
}
746746

747+
- (BOOL)wantsToHandleEventsAtPoint:(CGPoint)point
748+
{
749+
RNGHUIView *viewToHitTest = _recognizer.view;
750+
751+
if ([self usesNativeOrVirtualDetector] && [_recognizer.view.subviews count] > 0) {
752+
viewToHitTest = _recognizer.view.subviews[0];
753+
point = [_recognizer.view convertPoint:point toView:viewToHitTest];
754+
}
755+
756+
if (_actionType == RNGestureHandlerActionTypeVirtualDetector && _virtualViewTag != nil) {
757+
// In this case, logic detector is attached to the DetectorView, which has a single subview representing
758+
// the actual target view in the RN hierarchy
759+
if ([viewToHitTest respondsToSelector:@selector(touchEventEmitterAtPoint:)]) {
760+
// If the view has touchEventEmitterAtPoint: method, it can be used to determine the viewtag
761+
// of the view under the touch point
762+
facebook::react::SharedTouchEventEmitter eventEmitter =
763+
[(id<RCTTouchableComponentViewProtocol>)viewToHitTest touchEventEmitterAtPoint:point];
764+
auto viewUnderTouch = eventEmitter->getEventTarget()->getTag();
765+
766+
return viewUnderTouch == [_virtualViewTag intValue];
767+
}
768+
}
769+
770+
CGRect hitFrame = RNGHHitSlopInsetRect(viewToHitTest.bounds, _hitSlop);
771+
return CGRectContainsPoint(hitFrame, point);
772+
}
773+
747774
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
748775
{
749776
if ([_handlersToWaitFor count]) {

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -166,30 +166,6 @@ - (void)layout
166166
[self applyUnderlayCornerRadii];
167167
}
168168

169-
- (BOOL)shouldHandleTouch:(RNGHUIView *)view
170-
{
171-
if ([view isKindOfClass:[RNGestureHandlerButton class]]) {
172-
RNGestureHandlerButton *button = (RNGestureHandlerButton *)view;
173-
return button.userEnabled;
174-
}
175-
176-
// Certain subviews such as RCTViewComponentView have been observed to have disabled
177-
// accessibility gesture recognizers such as _UIAccessibilityHUDGateGestureRecognizer,
178-
// ostensibly set by iOS. Such gesture recognizers cause this function to return YES
179-
// even when the passed view is static text and does not respond to touches. This in
180-
// turn prevents the button from receiving touches, breaking functionality. To handle
181-
// such case, we can count only the enabled gesture recognizers when determining
182-
// whether a view should receive touches.
183-
NSPredicate *isEnabledPredicate = [NSPredicate predicateWithFormat:@"isEnabled == YES"];
184-
NSArray *enabledGestureRecognizers = [view.gestureRecognizers filteredArrayUsingPredicate:isEnabledPredicate];
185-
186-
#if !TARGET_OS_OSX
187-
return [view isKindOfClass:[UIControl class]] || [enabledGestureRecognizers count] > 0;
188-
#else
189-
return [view isKindOfClass:[NSControl class]] || [enabledGestureRecognizers count] > 0;
190-
#endif
191-
}
192-
193169
- (void)animateUnderlayToOpacity:(float)toOpacity duration:(NSTimeInterval)durationMs
194170
{
195171
_underlayLayer.opacity =
@@ -667,6 +643,44 @@ - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
667643
_isTouchInsideBounds = NO;
668644
}
669645

646+
- (BOOL)shouldHandleTouch:(RNGHUIView *)view atPoint:(CGPoint)point
647+
{
648+
if ([view isKindOfClass:[RNGestureHandlerButton class]]) {
649+
RNGestureHandlerButton *button = (RNGestureHandlerButton *)view;
650+
return button.userEnabled;
651+
}
652+
653+
// Certain subviews such as RCTViewComponentView have been observed to have disabled
654+
// accessibility gesture recognizers such as _UIAccessibilityHUDGateGestureRecognizer,
655+
// ostensibly set by iOS. Such gesture recognizers cause this function to return YES
656+
// even when the passed view is static text and does not respond to touches. This in
657+
// turn prevents the button from receiving touches, breaking functionality. To handle
658+
// such case, we can count only the enabled gesture recognizers when determining
659+
// whether a view should receive touches.
660+
NSPredicate *isEnabledPredicate = [NSPredicate predicateWithFormat:@"isEnabled == YES"];
661+
NSArray *enabledGestureRecognizers = [view.gestureRecognizers filteredArrayUsingPredicate:isEnabledPredicate];
662+
663+
BOOL gestureRecognizerWantsEvent = NO;
664+
for (UIGestureRecognizer *recognizer in enabledGestureRecognizers) {
665+
RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:recognizer];
666+
if (handler != nil) {
667+
CGPoint pointInView = [self convertPoint:point toView:view];
668+
gestureRecognizerWantsEvent = [handler wantsToHandleEventsAtPoint:pointInView];
669+
} else {
670+
gestureRecognizerWantsEvent = YES;
671+
}
672+
if (gestureRecognizerWantsEvent) {
673+
break;
674+
}
675+
}
676+
677+
#if !TARGET_OS_OSX
678+
return [view isKindOfClass:[UIControl class]] || gestureRecognizerWantsEvent;
679+
#else
680+
return [view isKindOfClass:[NSControl class]] || [enabledGestureRecognizers count] > 0;
681+
#endif
682+
}
683+
670684
- (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
671685
{
672686
RNGestureHandlerPointerEvents pointerEvents = _pointerEvents;
@@ -680,7 +694,7 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
680694
if (!subview.isHidden && subview.alpha > 0) {
681695
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
682696
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
683-
if (hitView != nil && [self shouldHandleTouch:hitView]) {
697+
if (hitView != nil && [self shouldHandleTouch:hitView atPoint:point]) {
684698
return hitView;
685699
}
686700
}
@@ -693,7 +707,7 @@ - (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
693707
}
694708

695709
RNGHUIView *inner = [super hitTest:point withEvent:event];
696-
while (inner && ![self shouldHandleTouch:inner]) {
710+
while (inner && ![self shouldHandleTouch:inner atPoint:point]) {
697711
inner = inner.superview;
698712
}
699713
return inner;

0 commit comments

Comments
 (0)