Skip to content

Commit 186eb6b

Browse files
authored
[iOS] Fix manualActivation being ignored after a config update mid-gesture (#4520)
## Description On iOS a handler with `manualActivation: true` activates on its own if its config is updated mid-gesture. Any re-render during the gesture triggers that, since the config is re-sent on every render. `setConfig:` runs `resetConfig` before `updateConfig:`, and `resetConfig` went through the `manualActivation` setter, which removed the `RNManualActivationRecognizer` from the view. `UIKit` cancels a recognizer removed mid-touch, so the failure requirement it imposed on the handler was lost and the new blocker created by `updateConfig:` never saw the touch. `resetConfig` now only clears the flag and the blocker is reconciled once after the config is applied, only when its presence no longer matches the flag. Android and web already reset values only. ## Test plan <details> <summary>Repro</summary> ```tsx import React, { useRef, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, GestureStateManager, usePanGesture, } from 'react-native-gesture-handler'; const ACTIVATION_THRESHOLD = 20; type Phase = 'idle' | 'began' | 'active'; const color = (phase: Phase) => phase === 'active' ? '#21a37c' : phase === 'began' ? '#f6b914' : '#9b59b6'; export default function EmptyExample() { const [phaseA, setPhaseA] = useState<Phase>('idle'); const [phaseB, setPhaseB] = useState<Phase>('idle'); const neverActivatePan = usePanGesture({ manualActivation: true, runOnJS: true, onBegin: () => { console.log('[A never-activate] onBegin'); setPhaseA('began'); }, onActivate: () => { console.log('[A never-activate] onActivate (SHOULD NOT HAPPEN)'); setPhaseA('active'); }, onFinalize: (e) => { console.log(`[A never-activate] onFinalize canceled=${e.canceled}`); setPhaseA('idle'); }, }); const start = useRef({ x: 0, y: 0 }); const requested = useRef(false); const selfActivatePan = usePanGesture({ manualActivation: true, runOnJS: true, onTouchesDown: (e) => { const t = e.allTouches[0]; start.current = { x: t.x, y: t.y }; requested.current = false; }, onTouchesMove: (e) => { if (requested.current) { return; } const t = e.allTouches[0]; const dx = t.x - start.current.x; const dy = t.y - start.current.y; if (Math.hypot(dx, dy) > ACTIVATION_THRESHOLD) { requested.current = true; GestureStateManager.activate(e.handlerTag); } }, onBegin: () => { console.log('[B self-activate] onBegin'); setPhaseB('began'); }, onActivate: () => { console.log('[B self-activate] onActivate'); setPhaseB('active'); }, onFinalize: (e) => { console.log(`[B self-activate] onFinalize canceled=${e.canceled}`); setPhaseB('idle'); }, }); return ( <View style={styles.container}> <Text style={styles.label}>A: manualActivation, never activated</Text> <GestureDetector gesture={neverActivatePan}> <View style={[styles.box, { backgroundColor: color(phaseA) }]} /> </GestureDetector> <Text style={styles.label}> B: manualActivation, activate() after {ACTIVATION_THRESHOLD}px </Text> <GestureDetector gesture={selfActivatePan}> <View style={[styles.box, { backgroundColor: color(phaseB) }]} /> </GestureDetector> <Text style={styles.hint}> Purple = idle, orange = BEGAN, green = ACTIVE. Drag each box. A must never turn green. </Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 8, }, label: { marginTop: 16, fontSize: 15, opacity: 0.6 }, hint: { marginTop: 24, fontSize: 13, opacity: 0.5, textAlign: 'center' }, box: { width: 150, height: 150, borderRadius: 12 }, }); ``` </details>
1 parent 2f688d8 commit 186eb6b

1 file changed

Lines changed: 14 additions & 3 deletions

File tree

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ - (void)resetConfig
116116
{
117117
self.enabled = YES;
118118
self.testID = nil;
119-
self.manualActivation = NO;
119+
_manualActivation = NO;
120120
_shouldCancelWhenOutside = NO;
121121
_cancelsJSResponder = YES;
122122
_hitSlop = RNGHHitSlopEmpty;
@@ -132,6 +132,7 @@ - (void)setConfig:(NSDictionary *)config
132132
{
133133
[self resetConfig];
134134
[self updateConfig:config];
135+
[self syncManualActivationRecognizer];
135136
}
136137

137138
- (void)updateConfig:(NSDictionary *)config
@@ -665,14 +666,24 @@ - (void)stopActivationBlocker
665666
- (void)setManualActivation:(BOOL)manualActivation
666667
{
667668
_manualActivation = manualActivation;
669+
[self syncManualActivationRecognizer];
670+
}
671+
672+
- (void)syncManualActivationRecognizer
673+
{
674+
BOOL hasRecognizer = _manualActivationRecognizer != nil;
668675

669-
if (manualActivation) {
676+
if (hasRecognizer == _manualActivation) {
677+
return;
678+
}
679+
680+
if (_manualActivation) {
670681
_manualActivationRecognizer = [[RNManualActivationRecognizer alloc] initWithGestureHandler:self];
671682

672683
if (_recognizer.view != nil) {
673684
[_recognizer.view addGestureRecognizer:_manualActivationRecognizer];
674685
}
675-
} else if (_manualActivationRecognizer != nil) {
686+
} else {
676687
[_manualActivationRecognizer.view removeGestureRecognizer:_manualActivationRecognizer];
677688
_manualActivationRecognizer = nil;
678689
}

0 commit comments

Comments
 (0)