Commit a52b1b4
feat(Tabs): add preventNativeSelection support (#3838)
## Description
Adds the ability to prevent native tab selection on a per-screen basis.
When `preventNativeSelection` is set to `true` on a `TabsScreen`,
tapping that tab in the tab bar (or selecting it from the "More" list on
iOS) will be blocked. The `TabsHost` receives an
`onTabSelectionPrevented` callback 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
software-mansion/react-native-screens-labs#1078
## Changes
- **`TabsScreen`**: New `preventNativeSelection` boolean prop plumbed
from JS → codegen specs → native on both Android and iOS.
- **`TabsHost`**: New `onTabSelectionPrevented` event (type + codegen
spec + native emitter) fired when selection is blocked.
- **iOS (`RNSTabBarController`)**:
- `tabBarController:shouldSelectViewController:` now checks per-screen
`preventNativeSelection` and emits the prevented event.
- ISA-swizzles `UIMoreNavigationController` to intercept
`pushViewController:animated:` so screens behind the "More" list are
also subject to prevention. Includes table-view deselection cleanup when
a push is blocked.
- Dynamic subclass name is derived from the actual runtime class (e.g.
`RNS_UIMoreNavigationController`), so each distinct original class gets
its own correct subclass — safe even if another library ISA-swizzles
first.
- Clears the `OBJC_ASSOCIATION_ASSIGN` back-reference in `dealloc` as a
safety measure against potential dangling pointers.
- Refactored `screenKeyForSelectedViewController` →
`screenKeyForViewController:` and
`isSelectedViewControllerTheMoreNavigationController` →
`isViewControllerTheMoreNavigationController:` to support querying
arbitrary view controllers (not just the selected one).
- **iOS (`RNSTabsScreenViewController`)**: Added `ScreenPropsForwarding`
category exposing `isPreventNativeSelectionEnabled`.
- **Android**: Prevention logic in `TabsContainer.onMenuItemSelected` —
checks `isPreventNativeSelectionEnabled` before progressing state,
delegates to `TabsContainerDelegate.onNavStateUpdatePrevented` which
emits the event via `TabsHost`.
- **Example app**: New test scenario (`TestTabsPreventNativeSelection`)
with 6 tabs, per-tab toggle, and toast on prevention.
## Known issues
- **`experimental_controlNavigationStateInJS` is 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 by `preventNativeSelection`). 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.
| iOS | Android |
| -- | -- |
| <video
src="https://github.com/user-attachments/assets/fed8b101-265e-469a-8e18-ad4cc6148618"
alt="s-1-ios" /> | <video
src="https://github.com/user-attachments/assets/523b0fbd-9366-4943-95bc-c4f7059a62e3"
alt="s-1-android" /> |
### 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
`OnTabSelectionPrevented` event*, as we effectively prevent s user from
navigating to that tab, even if this is not fully intentional. See [more
details
here](610a4df).
| Block from more list | Forced pop on navigation |
| -- | -- |
| <video
src="https://github.com/user-attachments/assets/67fb3e0e-b3bf-4ad8-8834-67e9cf8b439e"
alt="block-from-more-list" /> | <video
src="https://github.com/user-attachments/assets/d7302f29-dc27-4d9e-a335-6af5dcdbf2fc"
alt="forced-pop-on-navigation" /> |
## Test plan
- Run `TestTabsPreventNativeSelection` scenario from the example app.
- Tap a tab → verify it switches normally.
- Toggle `preventNativeSelection` on a tab → tap it → verify it does
**not** switch and the `onTabSelectionPrevented` toast appears.
- On iOS with 6 tabs: verify prevention also works for tabs listed under
the "More" navigation controller.
- Toggle the flag back off → verify the tab becomes selectable again.
## Checklist
- [x] Included code example that can be used to test this change.
- [ ] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [x] For API changes, updated relevant public types.
- [ ] Ensured that CI passes
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 parent d8528ec commit a52b1b4
File tree
26 files changed
+615
-35
lines changed- android/src/main/java/com/swmansion/rnscreens/gamma/tabs
- container
- host
- event
- screen
- apps/src/tests/single-feature-tests/tabs
- ios/tabs
- host
- screen
- src
- components/tabs
- host
- screen
- fabric/tabs
26 files changed
+615
-35
lines changedLines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
361 | 361 | | |
362 | 362 | | |
363 | 363 | | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
364 | 370 | | |
365 | 371 | | |
366 | 372 | | |
| |||
Lines changed: 13 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
35 | 48 | | |
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
185 | 192 | | |
186 | 193 | | |
187 | 194 | | |
| |||
Lines changed: 19 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
56 | 75 | | |
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| 63 | + | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
| |||
Lines changed: 47 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
| 98 | + | |
97 | 99 | | |
98 | 100 | | |
99 | 101 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
100 | 107 | | |
101 | 108 | | |
102 | 109 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
0 commit comments