Skip to content

Commit 560d1b5

Browse files
Fix GestureDetector unresponsive after display:none toggle (New Arch) (#3964)
## Description On New Architecture, when a parent view has display: 'none' and siblings change while hidden, the native UIView backing a component may be recycled and replaced. The React view tag stays the same but the UIView instance changes, causing gesture recognizers to be lost. This adds a reattachHandlersIfNeeded check in flushOperations that re-binds handlers whose native view has changed. The check is a fast no-op (pointer comparison) when views haven't been recycled. Fixes #3937 ## Test plan Tested with both v2 (Gesture.Tap()) and v3 (useTapGesture) APIs on iOS simulator (New Architecture). Minimal repro to verify the fix: ```ts import React, { useCallback, useState } from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import { Gesture, GestureDetector, useTapGesture, } from 'react-native-gesture-handler'; export default function DisplayNone() { const [tapCountV2, setTapCountV2] = useState(0); const [tapCountV3, setTapCountV3] = useState(0); const tapV2 = Gesture.Tap() .runOnJS(true) .onStart(() => { setTapCountV2((c) => c + 1); }); const tapV3 = useTapGesture({ runOnJS: true, onActivate: () => { setTapCountV3((c) => c + 1); }, }); const [visible, setVisible] = useState(true); const [randomViews, setRandomViews] = useState(100); const runTest = useCallback(() => { setVisible(false); setTimeout(() => { setRandomViews(Math.random() * 100); setTimeout(() => { setVisible(true); }, 500); }, 500); }, []); return ( <View style={styles.container}> <Text style={[ styles.status, { color: tapCountV2 > 0 ? 'green' : 'red', fontSize: 24 }, ]}> v2 Tap count: {tapCountV2} </Text> <Text style={[ styles.status, { color: tapCountV3 > 0 ? 'green' : 'red', fontSize: 24 }, ]}> v3 Tap count: {tapCountV3} </Text> <Button title="Run Test (Hide→Change→Show)" onPress={runTest} /> <View style={[ styles.wrapper, { display: visible ? 'flex' : 'none', }, ]}> <View style={styles.row}> {Array.from({ length: randomViews }).map((_, i) => ( <View key={i} style={styles.dot} /> ))} </View> <View style={styles.boxRow}> <GestureDetector gesture={tapV2}> <View style={styles.blueBox}> <Text style={styles.blueBoxText}>v2</Text> </View> </GestureDetector> <GestureDetector gesture={tapV3}> <View style={styles.greenBox}> <Text style={styles.blueBoxText}>v3</Text> </View> </GestureDetector> </View> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#ecf0f1', padding: 8, }, status: { fontSize: 16, fontWeight: 'bold', marginBottom: 20, textAlign: 'center', }, wrapper: { marginTop: 50, gap: 20, backgroundColor: 'yellow', }, row: { flexDirection: 'row', }, dot: { width: 2, height: 2, backgroundColor: 'black', }, boxRow: { flexDirection: 'row', gap: 20, }, blueBox: { height: 50, width: 50, backgroundColor: 'blue', justifyContent: 'center', alignItems: 'center', }, greenBox: { height: 50, width: 50, backgroundColor: 'green', justifyContent: 'center', alignItems: 'center', }, blueBoxText: { color: 'white', fontWeight: 'bold', }, }); ``` Steps: - Tap the box — count increments - Press "Hide → Change → Show" to trigger display:none + sibling change + show - Tap again — count should still increment (was broken before this fix) - Repeat multiple cycles to confirm reliability
1 parent 35cb78c commit 560d1b5

7 files changed

Lines changed: 65 additions & 11 deletions

File tree

packages/react-native-gesture-handler/apple/RNGestureHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
@property (nonatomic) BOOL dispatchesReanimatedEvents;
8686
@property (nonatomic, weak, nullable) RNGHUIView *hostDetectorView;
8787
@property (nonatomic, nullable, assign) NSNumber *virtualViewTag;
88+
@property (nonatomic, copy, nullable) NSNumber *viewTag;
8889

8990
- (BOOL)isViewParagraphComponent:(nullable RNGHUIView *)view;
9091
- (nonnull RNGHUIView *)chooseViewForInteraction:(nonnull UIGestureRecognizer *)recognizer;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ - (void)bindToView:(RNGHUIView *)view
262262

263263
[recognizerView addGestureRecognizer:self.recognizer];
264264
[self bindManualActivationToView:recognizerView];
265+
266+
self.viewTag = view.reactTag;
265267
}
266268

267269
- (void)unbindFromView
@@ -271,6 +273,7 @@ - (void)unbindFromView
271273

272274
self.hostDetectorView = nil;
273275
self.virtualViewTag = nil;
276+
self.viewTag = nil;
274277

275278
[self unbindManualActivation];
276279
}

packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@
4242
- (nullable RNGestureHandler *)handlerWithTag:(nonnull NSNumber *)handlerTag;
4343

4444
- (nullable RNGHUIView *)viewForReactTag:(nonnull NSNumber *)reactTag;
45+
46+
- (void)reattachHandlersIfNeeded;
4547
@end

packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ - (void)attachGestureHandler:(nonnull NSNumber *)handlerTag
171171
}
172172

173173
[_attachRetryCounter removeObjectForKey:viewTag];
174+
[self maybeBindHandler:handlerTag toViewWithTag:viewTag withActionType:actionType withHostDetector:hostDetector];
175+
}
176+
177+
// Resolves a view tag to its native UIView (including contentView unwrapping),
178+
// sets reactTag, and binds the gesture handler to it. No-op if the handler is
179+
// already attached to the correct view.
180+
- (void)maybeBindHandler:(nonnull NSNumber *)handlerTag
181+
toViewWithTag:(nonnull NSNumber *)viewTag
182+
withActionType:(RNGestureHandlerActionType)actionType
183+
withHostDetector:(nullable RNGHUIView *)hostDetector
184+
{
185+
RNGHUIView *view = [_viewRegistry viewForReactTag:viewTag];
186+
if (view == nil || view.superview == nil) {
187+
return;
188+
}
174189

175190
// I think it should be moved to RNNativeViewHandler, but that would require
176191
// additional logic for setting contentView.reactTag, this works for now
@@ -181,12 +196,17 @@ - (void)attachGestureHandler:(nonnull NSNumber *)handlerTag
181196
}
182197
}
183198

199+
RNGestureHandler *handler = [_registry handlerWithTag:handlerTag];
200+
201+
// Already attached to the correct native view, nothing to do.
202+
if (handler != nil && handler.recognizer.view == view && handler.actionType == actionType) {
203+
return;
204+
}
205+
184206
view.reactTag = viewTag; // necessary for RNReanimated eventHash (e.g. "42onGestureHandlerEvent"), also will be
185207
// returned as event.target
186208

187209
[_registry attachHandlerWithTag:handlerTag toView:view withActionType:actionType withHostDetector:hostDetector];
188-
189-
// register view if not already there
190210
[self registerViewWithGestureRecognizerAttachedIfNeeded:view];
191211
}
192212

@@ -224,6 +244,24 @@ - (id)handlerWithTag:(NSNumber *)handlerTag
224244
return [_registry handlerWithTag:handlerTag];
225245
}
226246

247+
- (void)reattachHandlersIfNeeded
248+
{
249+
// Re-bind handlers to their current native views. On Fabric, when a parent view has
250+
// display:none and siblings change, the native UIView backing a component may be recycled
251+
// and replaced. maybeBindHandler is a no-op if the view is nil or unchanged. This is only
252+
// needed for handlers using the old api.
253+
for (RNGestureHandler *handler in _registry.handlers.objectEnumerator) {
254+
if (handler.viewTag == nil || [handler usesNativeOrVirtualDetector]) {
255+
continue;
256+
}
257+
258+
[self maybeBindHandler:handler.tag
259+
toViewWithTag:handler.viewTag
260+
withActionType:handler.actionType
261+
withHostDetector:nil];
262+
}
263+
}
264+
227265
#pragma mark Root Views Management
228266

229267
- (void)registerViewWithGestureRecognizerAttachedIfNeeded:(RNGHUIView *)childView

packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,18 +171,21 @@ - (void)flushOperations
171171
{
172172
// On the new arch we rely on `flushOperations` for scheduling the operations on the UI thread.
173173
// On the old arch we rely on `uiManagerWillPerformMounting`
174-
if (_operations.count == 0) {
175-
return;
176-
}
177-
178174
RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
179-
NSArray<GestureHandlerOperation> *operations = _operations;
180-
_operations = [NSMutableArray new];
175+
176+
if (_operations.count > 0) {
177+
NSArray<GestureHandlerOperation> *operations = _operations;
178+
_operations = [NSMutableArray new];
179+
180+
[self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) {
181+
for (GestureHandlerOperation operation in operations) {
182+
operation(manager);
183+
}
184+
}];
185+
}
181186

182187
[self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) {
183-
for (GestureHandlerOperation operation in operations) {
184-
operation(manager);
185-
}
188+
[manager reattachHandlersIfNeeded];
186189
}];
187190
}
188191

packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@
2121
- (void)dropHandlerWithTag:(nonnull NSNumber *)handlerTag;
2222
- (void)dropAllHandlers;
2323

24+
@property (nonatomic, readonly, nonnull) NSDictionary<NSNumber *, RNGestureHandler *> *handlers;
25+
2426
@end

packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ - (instancetype)init
2222
return self;
2323
}
2424

25+
- (NSDictionary<NSNumber *, RNGestureHandler *> *)handlers
26+
{
27+
return _handlers;
28+
}
29+
2530
- (RNGestureHandler *)handlerWithTag:(NSNumber *)handlerTag
2631
{
2732
return _handlers[handlerTag];

0 commit comments

Comments
 (0)