diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm index af3d46fe31..86a537b00b 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm @@ -42,24 +42,10 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } -// TODO: I'm not sure whether this is the correct place for cleanup -// Possibly allowing recycling and doing this in prepareForRecycle would be better - (void)willMoveToWindow:(RNGHWindow *)newWindow { if (newWindow == nil) { - RNGestureHandlerManager *handlerManager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; - react_native_assert(handlerManager != nullptr && "Tried to access a non-existent handler manager"); - - [handlerManager.registry cancelAllObservationsForOwner:self]; - - for (NSNumber *handlerTag in _attachedHandlers) { - [handlerManager.registry detachHandlerWithTag:handlerTag fromHostDetector:self]; - } - - [_attachedHandlers removeAllObjects]; - _subscribedVirtualHandlers.clear(); - [_subscribedHandlers removeAllObjects]; - [_nativeHandlers removeAllObjects]; + [self detachAllHandlers]; } else { const auto &props = *std::static_pointer_cast(_props); [self attachHandlers:props.handlerTags @@ -70,6 +56,36 @@ - (void)willMoveToWindow:(RNGHWindow *)newWindow } } +// Detaching in `willMoveToWindow:` alone is not enough - a detector unmounted while +// its ancestor is already off-window (e.g. an inactive screen) never gets that call, +// and would enter the recycle pool still carrying recognizers of live handlers. +- (void)detachAllHandlers +{ + if (_moduleId == -1) { + return; + } + + RNGestureHandlerManager *handlerManager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; + if (handlerManager == nil) { + // A nil manager for a known moduleId means the module was invalidated mid-teardown + // and there is nothing left to clean up. An unknown moduleId is a real bug. + react_native_assert( + [RNGestureHandlerModule hasModuleWithId:_moduleId] && "Tried to access a non-existent handler manager"); + return; + } + + [handlerManager.registry cancelAllObservationsForOwner:self]; + + for (NSNumber *handlerTag in _attachedHandlers) { + [handlerManager.registry detachHandlerWithTag:handlerTag fromHostDetector:self]; + } + + [_attachedHandlers removeAllObjects]; + _subscribedVirtualHandlers.clear(); + [_subscribedHandlers removeAllObjects]; + [_nativeHandlers removeAllObjects]; +} + - (void)setDefaultProps { static const auto defaultProps = std::make_shared(); @@ -144,6 +160,7 @@ - (void)prepareForRecycle { [super prepareForRecycle]; + [self detachAllHandlers]; [self setDefaultProps]; } diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.h index 1268a63055..d4a515cb5d 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.h @@ -10,5 +10,6 @@ : RCTEventEmitter + (RNGestureHandlerManager *)handlerManagerForModuleId:(int)moduleId; ++ (BOOL)hasModuleWithId:(int)moduleId; @end diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm index 18659f0d27..231c897d04 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm @@ -50,7 +50,14 @@ @implementation RNGestureHandlerModule { + (RNGestureHandlerManager *)handlerManagerForModuleId:(int)moduleId { - return _managers[moduleId]; + // Not operator[] - that would insert a null entry on a miss. + const auto it = _managers.find(moduleId); + return it == _managers.end() ? nil : it->second; +} + ++ (BOOL)hasModuleWithId:(int)moduleId +{ + return _managers.find(moduleId) != _managers.end(); } RCT_EXPORT_MODULE() @@ -63,6 +70,11 @@ + (BOOL)requiresMainQueueSetup - (void)invalidate { RNGestureHandlerManager *handlerManager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId]; + + // Clear observations before the entry is nulled - the async drop below keeps the registry + // alive for one main-queue hop, and dropAllHandlers does not touch them. + [handlerManager.registry removeAllObservations]; + dispatch_async(dispatch_get_main_queue(), ^{ [handlerManager dropAllGestureHandlers]; }); diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h b/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h index 82649ed83c..8a1030536a 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h @@ -33,6 +33,7 @@ typedef void (^RNGestureHandlerReadyBlock)(RNGestureHandler *_Nonnull handler); - (void)cancelObservationForTag:(nonnull NSNumber *)handlerTag owner:(nonnull id)owner; - (void)cancelAllObservationsForOwner:(nonnull id)owner; +- (void)removeAllObservations; @property (nonatomic, readonly, nonnull) NSDictionary *handlers; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.m b/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.m index 558f0629b8..e56c3f009f 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.m +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.m @@ -97,6 +97,13 @@ - (void)cancelAllObservationsForOwner:(id)owner } } +- (void)removeAllObservations +{ + @synchronized(_handlers) { + [_observers removeAllObjects]; + } +} + - (void)attachHandlerWithTag:(NSNumber *)handlerTag toView:(RNGHUIView *)view withActionType:(RNGestureHandlerActionType)actionType