feat(Tabs): add preventNativeSelection support#3838
Conversation
Add boolean prop plumbing from JS to native on both Android and iOS. No logic attached yet — this will be used to implement tab selection prevention mechanism.
Add event plumbing for when native tab selection is prevented because the target screen has preventNativeSelection enabled. Payload carries current nav state (selectedScreenKey, provenance) and the prevented screen key. No emission logic yet — only types, codegen specs, event classes, and emitter methods.
When a tab has preventNativeSelection enabled, block native user taps from selecting it. Returns false from OnItemSelectedListener to prevent BottomNavigationView from updating its visual selection state, and emits onTabSelectionPrevented event to JS via TabsContainerDelegate chain.
- 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>
stack of more navigation controller In such cases, now we emit an `OnTabSelectionPrevented` event. I've thought about whether we should, but I came to conclusion that it is better to expose this information and let user ignore it, than not expose it at all, in the end we're effectively preventing user navigation to such tab. In the future we might consider adding some payload to indicate that the prevention happened exactly in such case, so that it is easier to ignore.
|
I've found a crash. Screen.Recording.2026-04-03.at.12.24.08.movNeed to investigate it next week. |
|
I've also noticed on @satya164 suggestion, that we don't emit an I do not know how to fix it yet, as we literally get no information from UIKit that the tab has been effectively changed - this sucks. |
|
@kkafar I was thinking, could
|
|
I've checked basic scenarios on iPhone 17pro (ios26.2) simulator:
I started test on iPad Pro simulator:
|
… isa resets UIKit resets the isa pointer of moreNavigationController back to the original UIMoreNavigationController class during tab bar reconfiguration (e.g. iPad multitasking resize crossing the >5 tab threshold). The instance stays the same, but our ISA-swizzle is wiped out, causing preventNativeSelection to stop working for screens in the More list after a resize cycle. Make ensurePushInterceptorOnMoreNavigationController idempotent — re-apply object_setClass + objc_setAssociatedObject on every call instead of guarding with a one-shot BOOL. The dynamic subclass is created once and reused; only the isa assignment is repeated. Rename _didInstallPushInterceptor to _didAccessMoreNavigationController to reflect its new purpose: guarding against lazy creation of moreNavigationController in dealloc.
|
Continued checks on iOS.
Screen.Recording.2026-04-07.at.09.48.16.mov
|
kligarski
left a comment
There was a problem hiding this comment.
This might be related to the bug you mentioned:
Screen.Recording.2026-04-07.at.10.25.25.mov
|
@satya164 could you elaborate what do you mean by "a separate thing"? I've initially considered exposing support for the On the other hand, I can see that incorporating navigation to Until we stabilise the API it's shape is modifiable. Waiting for any valid input here. Edit: Also I think I've found a way to get information from UIKit that it changes the tab from |
|
Android 16.0: PROBLEM:
On Android phone and tablet
|
|
@kkafar I meant something other than the controlled potential APIs maybe:
i'm unsure if a controlled API is appropriate for more navigation controller given that we can't actually control it consistently. |
… interceptor When UIKit does NOT reset the ISA of moreNavigationController between tab switches, ensurePushInterceptorOnMoreNavigationController would prepend another RNS_ layer (RNS_RNS_UIMoreNavigationController) on top of the existing swizzled class. The objc_msgSendSuper call in rns_pushViewController then resolved to the intermediate class which also carried our override, causing infinite recursion and a crash. Guard against this by checking the RNS_ prefix on the current ISA before attempting to create a new dynamic subclass. Also extract the associated-object setup into installSelfAssociationWithMoreNavigationController to keep the back-reference and access flag co-located.
|
@kligarski @LKuchno |
## Description `RNSTabsScreenViewController` overrides `viewWillAppear:`, `viewDidAppear:`, `viewWillDisappear:`, and `viewDidDisappear:` to emit React Native lifecycle events but none of them call their `super` implementation. Apple's documentation requires calling super in these methods — skipping it can break UIKit's internal bookkeeping for appearance transitions, trait collection propagation, and child container callbacks. ## Changes - Added `[super viewWillAppear:animated]`, `[super viewDidAppear:animated]`, `[super viewWillDisappear:animated]`, and `[super viewDidDisappear:animated]` calls before the event emission in each method. ## Test plan - Run any Tabs test scenario from the example app. - Verify tab switching, appearance/disappearance events, and orientation changes still work correctly. ## Checklist - [ ] Included code example that can be used to test this change. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [ ] Ensured that CI passes
|
I've opened a separate issue for the problem with sidebar. |
kligarski
left a comment
There was a problem hiding this comment.
Code looks good. I can check the runtime tomorrow.
Seems to be fixed now, the runtime on the iPhone simulator looks good. We can add a background color to tabs to avoid this glitch (iOS 18): Simulator.Screen.Recording.-.iPhone.16.Pro.-.2026-04-08.at.12.25.42.mov |
The ISA-swizzled rns_pushViewController on moreNavigationController used OBJC_ASSOCIATION_ASSIGN to store a back-reference to the owning RNSTabBarController. This required careful cleanup in dealloc to avoid dangling pointers. Since moreNavigationController is always a child of the tab bar controller, UIViewController.tabBarController already provides the reference via the parent chain. This eliminates the associated object, the dealloc cleanup, and the _didAccessMoreNavigationController guard.
Replace C-style casts with static_cast and reinterpret_cast. Extract the objc_msgSendSuper function pointer cast to a named local for readability.

Description
Adds the ability to prevent native tab selection on a per-screen basis. When
preventNativeSelectionis set totrueon aTabsScreen, tapping that tab in the tab bar (or selecting it from the "More" list on iOS) will be blocked. TheTabsHostreceives anonTabSelectionPreventedcallback with the key of the prevented screen and the current navigation state, allowing JS to decide how to handle the attempt (e.g. show a confirmation dialog, redirect elsewhere).Closes https://github.com/software-mansion/react-native-screens-labs/issues/1078
Changes
TabsScreen: NewpreventNativeSelectionboolean prop plumbed from JS → codegen specs → native on both Android and iOS.TabsHost: NewonTabSelectionPreventedevent (type + codegen spec + native emitter) fired when selection is blocked.RNSTabBarController):tabBarController:shouldSelectViewController:now checks per-screenpreventNativeSelectionand emits the prevented event.UIMoreNavigationControllerto interceptpushViewController:animated:so screens behind the "More" list are also subject to prevention. Includes table-view deselection cleanup when a push is blocked.RNS_UIMoreNavigationController), so each distinct original class gets its own correct subclass — safe even if another library ISA-swizzles first.OBJC_ASSOCIATION_ASSIGNback-reference indeallocas a safety measure against potential dangling pointers.screenKeyForSelectedViewController→screenKeyForViewController:andisSelectedViewControllerTheMoreNavigationController→isViewControllerTheMoreNavigationController:to support querying arbitrary view controllers (not just the selected one).RNSTabsScreenViewController): AddedScreenPropsForwardingcategory exposingisPreventNativeSelectionEnabled.TabsContainer.onMenuItemSelected— checksisPreventNativeSelectionEnabledbefore progressing state, delegates toTabsContainerDelegate.onNavStateUpdatePreventedwhich emits the event viaTabsHost.TestTabsPreventNativeSelection) with 6 tabs, per-tab toggle, and toast on prevention.Known issues
experimental_controlNavigationStateInJSis not respected in the "More" navigation controller flow on iOS. When the experimental controlled-mode flag is enabled and the user is already on the "More" tab, tapping items in the More list will not be blocked by the controlled-mode gate (only bypreventNativeSelection). This is acceptable since the controlled-mode feature is planned for removal before release.Visual documentation
Rudimentary scenarios
We navigate to a "third" tab natively, toggle
preventNativeSelection, go back, and navigate again. That attempt is blocked (prevented) and an event is emitted, as it can be observed by appearance of toast. We navigate to the "third" tab via JS - this is allowed, as we only prevent native selection - toggle the option and repeat the experiment. This time navigation to "third" tab is allowed.s-1-regular-tab.mov
s-1-android.mov
More navigation controller scenarios
In the video on the left I showcase that the navigation from "more list" is correctly prevented.
In the video on the right I cover a edge case, where a user / programmer navigates first the the "fifth" tab, then navigates away, e.g. to "third" and then comes back. W/o prevention mechanism, the "fifth" tab would be shown, as it is pushed onto more navigation controller stack. I've implemented logic to cover this case. Now, the "more list" will be displayed. I've decided that in such case we will emit the
OnTabSelectionPreventedevent, as we effectively prevent s user from navigating to that tab, even if this is not fully intentional. See more details here.s-2-more-nav-ctrl.mov
s-3-more-with-stack.mov
Test plan
TestTabsPreventNativeSelectionscenario from the example app.preventNativeSelectionon a tab → tap it → verify it does not switch and theonTabSelectionPreventedtoast appears.Checklist