SIGN IN SIGN UP

[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