Skip to content

Commit 64aea23

Browse files
kkafarclaude
andcommitted
fix(iOS): harden moreNavigationController push interceptor
- Clear OBJC_ASSOCIATION_ASSIGN back-reference in dealloc to avoid potential dangling pointer if moreNavigationController ever outlives the tab bar controller. - Derive dynamic subclass name from runtime class of moreNavigationController (e.g. RNS_UIMoreNavigationController) instead of using a static name, so each distinct original class gets its own correct subclass. - Move experimental_controlNavigationStateInJS check above preventNativeSelection to avoid firing onTabSelectionPrevented for the experimental controlled-mode path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b7281dd commit 64aea23

1 file changed

Lines changed: 44 additions & 11 deletions

File tree

ios/tabs/host/RNSTabBarController.mm

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ @implementation RNSTabBarController {
7070

7171
RNSTabsNavigationState *_Nullable _pendingOperation;
7272

73+
#if RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
74+
BOOL _didInstallPushInterceptor;
75+
#endif // RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
76+
7377
#if !RCT_NEW_ARCH_ENABLED
7478
BOOL _isControllerFlushBlockScheduled;
7579
#endif // !RCT_NEW_ARCH_ENABLED
@@ -102,6 +106,19 @@ - (instancetype)initWithTabsHostComponentView:(nullable RNSTabsHostComponentView
102106
return self;
103107
}
104108

109+
- (void)dealloc
110+
{
111+
#if RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
112+
// Clear the OBJC_ASSOCIATION_ASSIGN back-reference to self stored on moreNavigationController.
113+
// This is a safety measure — moreNavigationController should never outlive us, but if it ever does
114+
// (e.g. due to UIKit lifecycle changes), we avoid leaving a dangling pointer behind.
115+
if (_didInstallPushInterceptor) {
116+
objc_setAssociatedObject(
117+
self.moreNavigationController, kRNSTabBarControllerAssociationKey, nil, OBJC_ASSOCIATION_ASSIGN);
118+
}
119+
#endif // RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
120+
}
121+
105122
#pragma mark - UIKit callbacks
106123

107124
- (void)didMoveToParentViewController:(UIViewController *)parent
@@ -278,11 +295,6 @@ - (void)onDidPreventUserFromSelectingViewControllerWithKey:(nonnull NSString *)s
278295

279296
- (BOOL)shouldPreventNativeTabSelection:(nonnull UIViewController *)nextViewController
280297
{
281-
// This handles the tabsHostComponentView nullability
282-
if ([self.tabsHostComponentView experimental_controlNavigationStateInJS]) {
283-
return YES;
284-
}
285-
286298
if (![nextViewController isKindOfClass:RNSTabsScreenViewController.class]) {
287299
// Allow for more view controller selection
288300
return NO;
@@ -324,6 +336,13 @@ - (BOOL)tabBarController:(UITabBarController *)tabBarController
324336
return NO;
325337
}
326338

339+
// This handles the tabsHostComponentView nullability
340+
// TODO: This if is likely to be removed, since we want to roll back the support
341+
// for "controlled mode", at least initially.
342+
if ([self.tabsHostComponentView experimental_controlNavigationStateInJS]) {
343+
return NO;
344+
}
345+
327346
BOOL shouldPreventTabSelection = [self shouldPreventNativeTabSelection:viewController];
328347

329348
if (shouldPreventTabSelection) {
@@ -729,17 +748,30 @@ - (void)setupMoreNavigationControllerDelegateIfNeeded
729748

730749
/// Creates a dynamic subclass of the runtime class of `moreNavigationController`
731750
/// (which is the private `UIMoreNavigationController`) and overrides `pushViewController:animated:`
732-
/// with our gating implementation. The dynamic subclass is created once and reused.
751+
/// with our gating implementation.
752+
///
753+
/// The subclass name is derived from the actual runtime class of `moreNavigationController`
754+
/// (e.g. `RNS_UIMoreNavigationController`), so if another library ISA-swizzles it first or Apple
755+
/// changes the private class, each distinct original class gets its own correct dynamic subclass.
733756
- (void)installPushInterceptorOnMoreNavigationController
734757
{
735758
#if RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
736-
static const char *kDynamicSubclassName = "RNS_UIMoreNavigationController";
737-
Class dynamicSubclass = objc_getClass(kDynamicSubclassName);
759+
RCTAssert(
760+
_didInstallPushInterceptor == NO,
761+
@"[RNScreens] installPushInterceptorOnMoreNavigationController MUST NOT be called twice");
762+
763+
Class originalClass = object_getClass(self.moreNavigationController);
764+
const char *originalClassName = class_getName(originalClass);
765+
766+
// Build a unique subclass name per original runtime class: "RNS_<originalClassName>"
767+
char dynamicSubclassName[256];
768+
snprintf(dynamicSubclassName, sizeof(dynamicSubclassName), "RNS_%s", originalClassName);
769+
770+
Class dynamicSubclass = objc_getClass(dynamicSubclassName);
738771

739772
if (dynamicSubclass == nil) {
740-
Class originalClass = object_getClass(self.moreNavigationController);
741-
dynamicSubclass = objc_allocateClassPair(originalClass, kDynamicSubclassName, 0);
742-
RCTAssert(dynamicSubclass != nil, @"[RNScreens] Failed to allocate dynamic subclass of %@", originalClass);
773+
dynamicSubclass = objc_allocateClassPair(originalClass, dynamicSubclassName, 0);
774+
RCTAssert(dynamicSubclass != nil, @"[RNScreens] Failed to allocate dynamic subclass of %s", originalClassName);
743775

744776
Method pushMethod = class_getInstanceMethod(originalClass, @selector(pushViewController:animated:));
745777
class_addMethod(
@@ -757,6 +789,7 @@ - (void)installPushInterceptorOnMoreNavigationController
757789
// OBJC_ASSOCIATION_ASSIGN: no retain cycle — self owns moreNavigationController and outlives it.
758790
objc_setAssociatedObject(
759791
self.moreNavigationController, kRNSTabBarControllerAssociationKey, self, OBJC_ASSOCIATION_ASSIGN);
792+
_didInstallPushInterceptor = YES;
760793
#endif // RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
761794
}
762795

0 commit comments

Comments
 (0)