Skip to content

Commit 66a032a

Browse files
authored
[iOS] Keep NativeViewGestureHandler flags across a partial config update (#4527)
## Description `RNNativeViewGestureHandler updateConfig:` read `shouldActivateOnStart`, `disallowInterruption`, `yieldsToContinuousGestures` and `delaysChildPressedState` without `nil` checks, unlike every other handler. A full config set carries all keys, so it was fine. A partial `updateGestureHandlerConfig` carries only the changed key. After the first such update the three flags were `NO` and `delaysChildPressedState` was back to `YES`, which was also re-applied to the wrapped scroll view. They stayed wrong until the next React render of the owner re-sent the full config. Reproduced with a `RawButton` (`disallowInterruption`, `shouldCancelWhenOutside` bound to a SharedValue) inside a long-press detector: after each SharedValue change, the next press let the outer long press activate once, until the owner's re-render restored the flag. ## Test plan <details> <summary>Tested on the following code:</summary> ```tsx import React, { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, Pressable, RawButton, useLongPressGesture, } from 'react-native-gesture-handler'; import { useSharedValue } from 'react-native-reanimated'; // iOS. `RNNativeViewHandler updateConfig:` reads its four keys without nil // checks, so any partial update (here: the `shouldCancelWhenOutside` // SharedValue) resets `disallowInterruption` to NO. On iOS that flag only acts // in the UIControl touch-down path, and the v3 detector reaches a UIControl // only through RNGH's own button (Fabric TextInput/Switch wrappers hide theirs), // so the target is `RawButton`, whose wrapper forwards `disallowInterruption`. // With the flag set, touching the button cancels every other handler on the // touch, so the outer long press never activates. // // Steps: hold a finger on the button for ~1s. Expected: long press stays at 0 // (cancelled by the native handler) and native begins goes up by 1. Press // "Toggle SV" (sends the SharedValue false then true as partial updates), hold // again. Bug: long press activates. Fixed: still 0. const LONG_PRESS_MS = 400; export default function EmptyExample() { const cancelWhenOutside = useSharedValue(false); const [toggles, setToggles] = useState(0); const [nativeBegins, setNativeBegins] = useState(0); const [longPresses, setLongPresses] = useState(0); const longPress = useLongPressGesture({ runOnJS: true, minDuration: LONG_PRESS_MS, maxDistance: 50, onActivate: () => { setLongPresses((n) => n + 1); }, }); return ( <View style={styles.container}> <Text style={styles.label} testID="status"> SV toggles: {toggles}, native begins: {nativeBegins}, long presses:{' '} {longPresses} </Text> <Text style={styles.hint}> Hold on the button for ~1s. Each hold must add 1 to native begins (UIControl touch-down reached the handler). Before the toggle the long press must stay at 0. After the toggle it must still stay at 0 (bug: it counts). </Text> <Pressable style={styles.button} testID="toggle" onPress={() => { // Two partial updates; the flag ends where it started. cancelWhenOutside.value = true; cancelWhenOutside.value = false; setToggles((n) => n + 1); }}> <Text style={styles.buttonLabel}>Toggle SV</Text> </Pressable> <GestureDetector gesture={longPress}> <View style={styles.outer} testID="outer"> <Text style={styles.outerLabel}>outer: long press</Text> <RawButton style={styles.input} testID="button" disallowInterruption shouldCancelWhenOutside={cancelWhenOutside} onBegin={() => { setNativeBegins((n) => n + 1); }}> <Text style={styles.inputLabel}> RawButton, disallowInterruption </Text> </RawButton> </View> </GestureDetector> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', padding: 16, }, label: { fontSize: 14, color: '#001a72', marginVertical: 4, textAlign: 'center', }, hint: { fontSize: 12, color: '#555', marginVertical: 4, textAlign: 'center', }, button: { marginVertical: 12, paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8, backgroundColor: '#782aeb', }, buttonLabel: { color: 'white', fontSize: 16, }, outer: { marginTop: 12, width: 300, padding: 24, borderRadius: 16, backgroundColor: '#f6b914', alignItems: 'center', }, outerLabel: { color: 'white', fontSize: 16, marginBottom: 12, }, input: { width: 240, height: 56, borderRadius: 8, backgroundColor: 'white', justifyContent: 'center', alignItems: 'center', }, inputLabel: { color: '#001a72', fontSize: 14, }, }); ``` </details>
1 parent 08c9e7c commit 66a032a

1 file changed

Lines changed: 36 additions & 10 deletions

File tree

‎packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm‎

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,25 +132,51 @@ - (instancetype)initWithTag:(NSNumber *)tag
132132
return self;
133133
}
134134

135-
- (void)updateConfig:(NSDictionary *)config
135+
- (void)resetConfig
136136
{
137-
[super updateConfig:config];
138-
_shouldActivateOnStart = [RCTConvert BOOL:config[@"shouldActivateOnStart"]];
139-
_disallowInterruption = [RCTConvert BOOL:config[@"disallowInterruption"]];
140-
_yieldsToContinuousGestures = [RCTConvert BOOL:config[@"yieldsToContinuousGestures"]];
141-
142-
id delaysChildPressedState = config[@"delaysChildPressedState"];
143-
_delaysChildPressedState = delaysChildPressedState == nil ? YES : [RCTConvert BOOL:delaysChildPressedState];
137+
[super resetConfig];
138+
_shouldActivateOnStart = NO;
139+
_disallowInterruption = NO;
140+
_yieldsToContinuousGestures = NO;
141+
_delaysChildPressedState = YES;
142+
[self applyDelaysChildPressedState];
143+
}
144144

145+
- (void)applyDelaysChildPressedState
146+
{
145147
#if !TARGET_OS_OSX
146-
// Config may be updated after the handler is bound to a view — re-apply to the connected
147-
// scroll view if there is one.
148148
if (self.recognizer.view != nil) {
149149
[self retrieveScrollView:self.recognizer.view].delaysContentTouches = _delaysChildPressedState;
150150
}
151151
#endif
152152
}
153153

154+
- (void)updateConfig:(NSDictionary *)config
155+
{
156+
[super updateConfig:config];
157+
158+
id prop = config[@"shouldActivateOnStart"];
159+
if (prop != nil) {
160+
_shouldActivateOnStart = [RCTConvert BOOL:prop];
161+
}
162+
163+
prop = config[@"disallowInterruption"];
164+
if (prop != nil) {
165+
_disallowInterruption = [RCTConvert BOOL:prop];
166+
}
167+
168+
prop = config[@"yieldsToContinuousGestures"];
169+
if (prop != nil) {
170+
_yieldsToContinuousGestures = [RCTConvert BOOL:prop];
171+
}
172+
173+
prop = config[@"delaysChildPressedState"];
174+
if (prop != nil) {
175+
_delaysChildPressedState = [RCTConvert BOOL:prop];
176+
[self applyDelaysChildPressedState];
177+
}
178+
}
179+
154180
- (void)bindToView:(RNGHUIView *)view
155181
{
156182
_boundView = view;

0 commit comments

Comments
 (0)