Skip to content

Commit 8ec2529

Browse files
authored
[iOS] Fix gestures not ending properly on iOS 26 (#3740)
## 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](https://github.com/software-mansion/react-native-gesture-handler/blob/21c4943d5769d3fff60f7bf0550c5810f6011e13/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m#L120)). Looks like it was called twice for some reason (but I believe that [check for _lastState](https://github.com/software-mansion/react-native-gesture-handler/blob/21c4943d5769d3fff60f7bf0550c5810f6011e13/packages/react-native-gesture-handler/apple/RNGestureHandler.mm#L311) 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: - [x] iOS 26.0 (iPhone 17 Pro) - [x] iOS 18.5 (iPhone 16e) - [x] OSX (macOS 15.6.1) <details> <summary>Test 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 .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>
1 parent f7209a7 commit 8ec2529

File tree

4 files changed

+22
-9
lines changed

4 files changed

+22
-9
lines changed

packages/react-native-gesture-handler/apple/Handlers/RNFlingHandler.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,17 @@ - (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
5353
_lastPoint = [[[touches allObjects] objectAtIndex:0] locationInView:_gestureHandler.recognizer.view];
5454
[super touchesEnded:touches withEvent:event];
5555
[_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];
56+
57+
[self triggerAction];
5658
}
5759

5860
- (void)touchesCancelled:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
5961
{
6062
_lastPoint = [[[touches allObjects] objectAtIndex:0] locationInView:_gestureHandler.recognizer.view];
6163
[super touchesCancelled:touches withEvent:event];
6264
[_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];
65+
66+
[self triggerAction];
6367
}
6468

6569
- (void)triggerAction
@@ -69,7 +73,6 @@ - (void)triggerAction
6973

7074
- (void)reset
7175
{
72-
[self triggerAction];
7376
[_gestureHandler.pointerTracker reset];
7477
_hasBegan = NO;
7578
[super reset];

packages/react-native-gesture-handler/apple/Handlers/RNLongPressHandler.m

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,16 @@ - (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
102102
{
103103
[super touchesEnded:touches withEvent:event];
104104
[_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];
105+
106+
[self triggerAction];
105107
}
106108

107109
- (void)touchesCancelled:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
108110
{
109111
[super touchesCancelled:touches withEvent:event];
110112
[_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];
113+
114+
[self triggerAction];
111115
}
112116

113117
#else
@@ -181,10 +185,6 @@ - (BOOL)shouldCancelGesture
181185

182186
- (void)reset
183187
{
184-
if (self.state == UIGestureRecognizerStateFailed) {
185-
[self triggerAction];
186-
}
187-
188188
[_gestureHandler.pointerTracker reset];
189189

190190
[super reset];

packages/react-native-gesture-handler/apple/Handlers/RNPanHandler.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ - (void)interactionsMoved:(NSSet *)touches withEvent:(UIEvent *)event
165165
// UIGestureRecognizerStateFailed here. Making the behavior explicit.
166166
self.state = (self.state == UIGestureRecognizerStatePossible) ? UIGestureRecognizerStateFailed
167167
: UIGestureRecognizerStateCancelled;
168+
169+
[self triggerAction];
168170
[self reset];
169171
return;
170172
}
@@ -243,19 +245,22 @@ - (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
243245
{
244246
[super touchesEnded:touches withEvent:event];
245247
[self interactionsEnded:touches withEvent:event];
248+
249+
[self triggerAction];
246250
}
247251

248252
- (void)touchesCancelled:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
249253
{
250254
[super touchesCancelled:touches withEvent:event];
251255
[self interactionsCancelled:touches withEvent:event];
256+
257+
[self triggerAction];
252258
}
253259

254260
#endif
255261

256262
- (void)reset
257263
{
258-
[self triggerAction];
259264
[_gestureHandler.pointerTracker reset];
260265
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(activateAfterLongPress) object:nil];
261266
self.enabled = YES;

packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ - (void)interactionsEnded:(NSSet *)touches withEvent:(UIEvent *)event
131131

132132
if (_numberOfTaps == _tapsSoFar && _maxNumberOfTouches >= _minPointers) {
133133
self.state = UIGestureRecognizerStateEnded;
134+
135+
[self triggerAction];
134136
[self reset];
135137
} else {
136138
[self performSelector:@selector(cancel) withObject:nil afterDelay:_maxDelay];
@@ -141,6 +143,8 @@ - (void)interactionsCancelled:(NSSet *)touches withEvent:(UIEvent *)event
141143
{
142144
[_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];
143145
self.state = UIGestureRecognizerStateCancelled;
146+
147+
[self triggerAction];
144148
[self reset];
145149
}
146150

@@ -200,12 +204,16 @@ - (void)touchesEnded:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
200204
{
201205
[super touchesEnded:touches withEvent:event];
202206
[self interactionsEnded:touches withEvent:event];
207+
208+
[self triggerAction];
203209
}
204210

205211
- (void)touchesCancelled:(NSSet<RNGHUITouch *> *)touches withEvent:(UIEvent *)event
206212
{
207213
[super touchesCancelled:touches withEvent:event];
208214
[self interactionsCancelled:touches withEvent:event];
215+
216+
[self triggerAction];
209217
}
210218

211219
#endif
@@ -243,9 +251,6 @@ - (BOOL)shouldFailUnderCustomCriteria
243251

244252
- (void)reset
245253
{
246-
if (self.state == UIGestureRecognizerStateFailed) {
247-
[self triggerAction];
248-
}
249254
[_gestureHandler.pointerTracker reset];
250255

251256
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cancel) object:nil];

0 commit comments

Comments
 (0)