Skip to content

Conversation

@m-bert
Copy link
Contributor

@m-bert m-bert commented Oct 7, 2025

Description

There's a change in gesture recognizers introduced in iOS 26. Now when reset method is called, recognizers go back to UIGestureRecognizerStatePossible state. This breaks our current behavior, because this state is mapped into RNGestureHandlerStateBegan, so if for example Pan fails, it tries to send event with Began state.

Unfortunately, changing recognizer state is not possible outside of touches* methods, therefore we had to move triggerAction into those callbacks.

Let me know if you see a different approach into this problem.

Fixes #3733

Warning

triggerAction call was already present in Tap right before reset (see here). Looks like it was called twice for some reason (but I believe that check for _lastState prevented any problems with this redundancy). For now I have not included second call. If you think it is required, let me know.

Test plan

Tested on the code provided below, on the following platforms:

  • iOS 26.0 (iPhone 17 Pro)
  • iOS 18.5 (iPhone 16e)
  • OSX (macOS 15.6.1)
Test code:
import { StyleSheet, View, Text } from 'react-native';

import {
  GestureHandlerRootView,
  Gesture,
  GestureDetector,
  GestureType,
} from 'react-native-gesture-handler';

function TestBox({
  gestureType,
  bgColor,
}: {
  gestureType: GestureType;
  bgColor: string;
}) {
  const handlerName = gestureType.handlerName;

  const gesture = gestureType
    .onEnd(() => {
      console.log(`[${handlerName}] onEnd`);
    })
    .onFinalize(() => {
      console.log(`[${handlerName}] onFinalize`);
    })
    .runOnJS(true);

  return (
    <View style={styles.center}>
      <Text>{handlerName}</Text>
      <GestureDetector gesture={gesture}>
        <View style={[styles.box, { backgroundColor: bgColor }]} />
      </GestureDetector>
    </View>
  );
}

export default function App() {
  return (
    <GestureHandlerRootView style={[{ flex: 1, padding: 50 }, styles.center]}>
      <TestBox gestureType={Gesture.Pan()} bgColor="#b58df1" />
      <TestBox gestureType={Gesture.LongPress()} bgColor="#f1a85d" />
      <TestBox gestureType={Gesture.Fling()} bgColor="#5df1a8" />
      <TestBox gestureType={Gesture.Tap()} bgColor="#5d8ef1" />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  center: {
    display: 'flex',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  box: {
    height: 100,
    width: 100,
    backgroundColor: '#b58df1',
    borderRadius: 20,
    marginBottom: 30,
  },
});

#define RNGHGestureRecognizerStateBegan UIGestureRecognizerStateBegan;
#define RNGHGestureRecognizerStateEnded UIGestureRecognizerStateEnded;

#define iOS_VERSION [[[UIDevice currentDevice] systemVersion] floatValue]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, since iOS_VERSION macro returns float, we may want to do comparisons up to a constant $\epsilon$, or round this value. I don't think it is necessary at this point, but let me know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont't really see a case where this would be an issue. I know floats behave weirdly, but we will always be checking it via a \leq or \geq comparison with another float and and the mapping from real values to floats is monotonic - it keeps the ordering.

@m-bert m-bert marked this pull request as ready for review October 8, 2025 15:35
@m-bert m-bert requested a review from akwasniewski October 8, 2025 15:56
#define RNGHGestureRecognizerStateBegan UIGestureRecognizerStateBegan;
#define RNGHGestureRecognizerStateEnded UIGestureRecognizerStateEnded;

#define iOS_VERSION [[[UIDevice currentDevice] systemVersion] floatValue]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont't really see a case where this would be an issue. I know floats behave weirdly, but we will always be checking it via a \leq or \geq comparison with another float and and the mapping from real values to floats is monotonic - it keeps the ordering.

@akwasniewski
Copy link
Contributor

akwasniewski commented Oct 9, 2025

I'll just mention that we might want to release this fix fast, in a release, as currently pan is broken on all iOS 26 devices.

@mysport12
Copy link
Contributor

Any chance this can get merged and pushed in a new release?

@sabicd
Copy link

sabicd commented Oct 15, 2025

@akwasniewski hey there - is there a timeline for a new release?

@m-bert
Copy link
Contributor Author

m-bert commented Oct 15, 2025

Any chance this can get merged and pushed in a new release?

I can't give you exact ETA, but it should be the beginning of the next week.

@m-bert m-bert requested a review from j-piasecki October 15, 2025 16:34
@m-bert m-bert merged commit 8ec2529 into main Oct 17, 2025
4 checks passed
@m-bert m-bert deleted the @mbert/fix-states-iOS-26 branch October 17, 2025 14:43
m-bert added a commit that referenced this pull request Oct 20, 2025
## Description

Some changes in #3740 required follow up.

#### Tap

Turns out that we can leave `triggerAction` in `reset` (thank you apple
for transparent docs that clearly describe what changed between 18.5 and
26 ❤️).

It also seems that double `onFinalize` issue is fixed on 26.

#### Fling

Removing `[self triggerAction]` from `reset` broke things on old iOS.
Unfortunately now swiping in different direction does not result in
calling `onFinalize` - this should be handled by `triggerAction` in
`reset`, but now by default recognizers in `reset` have `Possible` state
(once again, thank you apple 😄). To fix that we will probably have to
rewrite fling to custom logic.

#### LongPress

`LongPress` was missing on `triggerAction` call that sends `fail`.

#### Pan 

In `Pan`, I've moved `triggerAction` calls from `touches*` methods to
`interactions*` methods.


## Test plan

<details>
<summary>Tested on basic-example and the following code:</summary>

```tsx
import { StyleSheet, View, Text } from 'react-native';

import {
  GestureHandlerRootView,
  Gesture,
  GestureDetector,
  GestureType,
} from 'react-native-gesture-handler';

function TestBox({
  gestureType,
  bgColor,
}: {
  gestureType: GestureType;
  bgColor: string;
}) {
  const handlerName = gestureType.handlerName;

  const gesture = gestureType
    .onBegin(() => {
      console.log(`[${handlerName}] onBegin`);
    })
    .onEnd(() => {
      console.log(`[${handlerName}] onEnd`);
    })
    .onFinalize(() => {
      console.log(`[${handlerName}] onFinalize`);
    })
    .runOnJS(true);

  return (
    <View style={styles.center}>
      <Text>{handlerName}</Text>
      <GestureDetector gesture={gesture}>
        <View style={[styles.box, { backgroundColor: bgColor }]} />
      </GestureDetector>
    </View>
  );
}

export default function App() {
  return (
    <GestureHandlerRootView style={[{ flex: 1, padding: 50 }, styles.center]}>
      <TestBox gestureType={Gesture.Pan()} bgColor="#b58df1" />
      <TestBox gestureType={Gesture.LongPress()} bgColor="#f1a85d" />
      <TestBox gestureType={Gesture.Fling()} bgColor="#5df1a8" />
      <TestBox gestureType={Gesture.Tap()} bgColor="#5d8ef1" />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  center: {
    display: 'flex',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  box: {
    height: 100,
    width: 100,
    backgroundColor: '#b58df1',
    borderRadius: 20,
    marginBottom: 30,
  },
});
```

</details>

---------

Co-authored-by: Jakub Piasecki <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS 26: Pan gesture events don't work properly

5 participants