Repository navigation
Commit 66a032a
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
144 | 144 | | |
| 145 | + | |
| 146 | + | |
145 | 147 | | |
146 | | - | |
147 | | - | |
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
154 | 180 | | |
155 | 181 | | |
156 | 182 | | |
| |||
0 commit comments