[iOS] Fix gestures attached via `VirtualGestureDetector` never recognizing continuous gestures (#4393)
## Description
A continuous gesture (e.g. `usePanGesture`) attached through
`VirtualGestureDetector` to a text span never recognized on iOS - no
callbacks at all. Android and web work, and a virtual tap works on iOS.
`RNBetterPanGestureRecognizer` consults `gestureRecognizerShouldBegin:`
manually from `touchesBegan`, before the recognizer has processed the
touch. `locationInView:` returns a stale point at that moment, so the
virtual hit-test fails and the pan sets itself to `Failed` on
touch-down. Taps are unaffected because UIKit consults their
`shouldBegin` at recognition time, when the location is valid.
The virtual gate now lives in `gestureRecognizer:shouldReceiveTouch:`,
where the touch's own location is available - a touch that doesn't start
on the virtual view never reaches the recognizer, matching how Android
routes by the initial touch position. Deferring the check to the `Began`
transition instead would not work: the pointer may have legitimately
left a small span by then, and the pan would also begin for touches from
other spans. The `shouldBegin` check remains for macOS only, which has
no `shouldReceiveTouch` equivalent. The condition and hit-test are
extracted into `hasVirtualTarget` / `virtualTargetContainsPoint:`,
shared by all call sites.
## Test plan
<details>
<summary>Minimal reproduction:</summary>
```tsx
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
InterceptingGestureDetector,
usePanGesture,
VirtualGestureDetector,
} from 'react-native-gesture-handler';
export default function EmptyExample() {
const spanPan = usePanGesture({
onBegin: () => console.log('[span-pan] onBegin'),
onActivate: () => console.log('[span-pan] onActivate'),
onDeactivate: () => console.log('[span-pan] onDeactivate'),
onFinalize: () => console.log('[span-pan] onFinalize'),
});
const viewPan = usePanGesture({
onBegin: () => console.log('[view-pan] onBegin'),
onActivate: () => console.log('[view-pan] onActivate'),
onDeactivate: () => console.log('[view-pan] onDeactivate'),
onFinalize: () => console.log('[view-pan] onFinalize'),
});
return (
<View style={styles.container}>
{/* Bug: pan on a folded (virtual) text span — silent on iOS before this fix */}
<InterceptingGestureDetector>
<Text style={styles.paragraph}>
<VirtualGestureDetector gesture={spanPan}>
<Text style={styles.span}>DRAG ME (virtual text span)</Text>
</VirtualGestureDetector>
</Text>
</InterceptingGestureDetector>
{/* Control: real View child — attaches directly, works with and without the fix */}
<InterceptingGestureDetector>
<VirtualGestureDetector gesture={viewPan}>
<View style={styles.box}>
<Text style={styles.boxLabel}>drag me (real View — control)</Text>
</View>
</VirtualGestureDetector>
</InterceptingGestureDetector>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 32, padding: 24 },
paragraph: { fontSize: 18, textAlign: 'center' },
span: { fontSize: 22, fontWeight: 'bold', color: 'mediumvioletred' },
box: {
width: 240,
height: 100,
borderRadius: 12,
backgroundColor: 'steelblue',
justifyContent: 'center',
alignItems: 'center',
},
boxLabel: { color: 'white' },
});
```
</details>
---------
Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com> M
Michał Bert committed
769b2ec7fd311050329ca1ff9ac1c7a8f0e8a750
Parent: 7b35938
Committed by GitHub <noreply@github.com>
on 8/7/2026, 9:53:18 AM