From e07872cd8e5c2fc1db2b9342c7a650a26d2e51e6 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai Date: Thu, 30 Oct 2025 23:11:10 +0100 Subject: [PATCH 1/2] fix(ios): improve menu view child component handling properly clean up menu button and restore disabled views when unmounting track disabled views in a set to avoid duplicate state changes --- ios/MenuView.mm | 51 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/ios/MenuView.mm b/ios/MenuView.mm index 228583b..945cd7d 100644 --- a/ios/MenuView.mm +++ b/ios/MenuView.mm @@ -19,6 +19,8 @@ @implementation MenuView { UIColor *_textColor; UIColor *_checkedColor; UIColor *_uncheckedColor; + BOOL _isChildViewButton; + NSMutableSet *_disabledViews; } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -31,6 +33,8 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _props = defaultProps; + _disabledViews = [[NSMutableSet alloc] init]; + _isChildViewButton = NO; } return self; @@ -42,10 +46,8 @@ - (void)mountChildComponentView:(UIView *)childCompone if (index == 0) { // Clean up old child view if exists if (_childView && _childView != childComponentView) { - // Remove from our manual tracking without calling removeFromSuperview - // since React will handle the view hierarchy cleanup - _childView = nil; - _menuButton = nil; + [self cleanupMenuButton]; + [self restoreUserInteractionForDisabledViews]; } _childView = (UIView *)childComponentView; @@ -64,8 +66,9 @@ - (void)unmountChildComponentView:(UIView *)childCompo { // Clean up our references before React unmounts if (index == 0 && _childView == childComponentView) { + [self cleanupMenuButton]; + [self restoreUserInteractionForDisabledViews]; _childView = nil; - _menuButton = nil; } // Let React handle the unmounting @@ -78,9 +81,11 @@ - (void)setupChildViewAsMenuTrigger:(UIView *)childView if ([childView isKindOfClass:[UIButton class]]) { _menuButton = (UIButton *)childView; _menuButton.showsMenuAsPrimaryAction = YES; + _isChildViewButton = YES; [self updateMenuItems:_menuItems selectedIdentifier:nil]; } else { // For non-button children, create an invisible button overlay to show the menu + _isChildViewButton = NO; [self disableUserInteractionRecursively:childView]; // Create an invisible button that covers the entire view @@ -105,12 +110,46 @@ - (void)setupChildViewAsMenuTrigger:(UIView *)childView - (void)disableUserInteractionRecursively:(UIView *)view { - view.userInteractionEnabled = NO; + if (view.userInteractionEnabled) { + [_disabledViews addObject:view]; + view.userInteractionEnabled = NO; + } for (UIView *subview in view.subviews) { [self disableUserInteractionRecursively:subview]; } } +- (void)restoreUserInteractionForDisabledViews +{ + for (UIView *view in _disabledViews) { + // Only restore if the view is still in the view hierarchy + if (view.superview != nil) { + view.userInteractionEnabled = YES; + } + } + [_disabledViews removeAllObjects]; +} + +- (void)cleanupMenuButton +{ + if (_menuButton) { + // If it's not a child view button (i.e., it's our overlay button), remove it + if (!_isChildViewButton && _menuButton.superview == self) { + [_menuButton removeFromSuperview]; + } + // Clear the menu to prevent any lingering references + _menuButton.menu = nil; + _menuButton = nil; + } + _isChildViewButton = NO; +} + +- (void)dealloc +{ + [self cleanupMenuButton]; + [self restoreUserInteractionForDisabledViews]; +} + - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { const auto &oldViewProps = *std::static_pointer_cast(_props); From ea66a7b852175bae9e506a71f5418294ae732a30 Mon Sep 17 00:00:00 2001 From: Ahmed Sbai Date: Thu, 30 Oct 2025 23:19:42 +0100 Subject: [PATCH 2/2] refactor(MenuView): replace NSMutableSet with NSHashTable for disabled views Use NSHashTable with weak references to prevent memory leaks when views are deallocated. Add safety checks during iteration to handle weak references properly. --- ios/MenuView.mm | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ios/MenuView.mm b/ios/MenuView.mm index 945cd7d..85d46c6 100644 --- a/ios/MenuView.mm +++ b/ios/MenuView.mm @@ -20,7 +20,7 @@ @implementation MenuView { UIColor *_checkedColor; UIColor *_uncheckedColor; BOOL _isChildViewButton; - NSMutableSet *_disabledViews; + NSHashTable *_disabledViews; } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -33,7 +33,7 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _props = defaultProps; - _disabledViews = [[NSMutableSet alloc] init]; + _disabledViews = [NSHashTable weakObjectsHashTable]; _isChildViewButton = NO; } @@ -121,9 +121,13 @@ - (void)disableUserInteractionRecursively:(UIView *)view - (void)restoreUserInteractionForDisabledViews { - for (UIView *view in _disabledViews) { - // Only restore if the view is still in the view hierarchy - if (view.superview != nil) { + // Create a copy of the objects to iterate over since NSHashTable with weak references + // can have objects deallocated during iteration + NSArray *viewsToRestore = [_disabledViews allObjects]; + + for (UIView *view in viewsToRestore) { + // The view might have been deallocated (weak reference), so check if it's still valid + if (view && view.superview != nil) { view.userInteractionEnabled = YES; } }