Skip to content

fix(iOS, Tabs): filter UIKit's didSelectTab: for programmatic selection while More is active - #4890

Merged
t0maboro merged 1 commit into
mainfrom
@t0maboro/fix-more-programmatic-selection-duplicate
Oct 13, 2026
Merged

t0maboro merged 1 commit into
mainfrom
@t0maboro/fix-more-programmatic-selection-duplicate

Conversation

@t0maboro

@t0maboro t0maboro commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

(generated by Claude)

Description

On the UITab API path (iOS 26.1+) a programmatic tab selection made while the More tab is active emits onTabSelected twice: a user-origin event first, then the programmatic-js one, and the navigation state provenance skips a value. It happens when the current tab overflows into More and was itself selected programmatically (e.g. Select Sixth from JS, then Select Second). Entering More through the More list and then selecting programmatically gives a single event. Found during the 4.30.0 release testing (#4884), regression from #4862; 4.29.0 and 4.28.0 emit one event.

Root cause: -[RNSTabBarController applySelectedScreenController:] sets _isApplyingProgrammaticTabSelection only around self.selectedTab = …, the branch taken when the More item is NOT selected. While More is selected it falls back to [self setSelectedViewController:] without the flag. UIKit sends tabBarController:didSelectTab:previousTab: synchronously from inside that write, and since #4862 every unflagged didSelectTab: is treated as a user selection: userDidSelectViewController: progresses the state once more and emits a user event, after which the programmatic path emits its own event with the already-bumped provenance. Before #4862 the filter worked the other way round (flag set in shouldSelectTab:), so the programmatic setSelectedViewController: write was ignored.

Changes

  • RNSTabBarController -applySelectedScreenController:: _isApplyingProgrammaticTabSelection now wraps the whole UITab branch, including the setSelectedViewController: fallback used while More is active. Same pattern as installScreenControllers: around setTabs: / selectedTab.

Before & after - visual documentation

Before After
before.mov
after.mov

Test plan

  • tabs/test-tabs-more-navigation-controller-ios (SFT), iPhone simulator, iOS 27.0 and 26.5: Select Sixth, then Select Second. Before: two onTabSelected toasts for Second; after: one, with the provenance advanced by exactly one (check the onTabSelected payload). Also More → Fifth → Select First (one event, unchanged) and user taps between More and regular tabs (unchanged).
  • tabs-stack-v4/test-tabs-search-tab-activation-ios on iOS 27.0, the fix(iOS, Tabs): don't drop user selection of UISearchTab on mount transactions #4862 scenario: reselecting the search tab through the collapsed tab bar still shows "Selected in JS: yes".
  • tabs/test-tabs-navigation-state-search-tab and tabs/test-tabs-simple-nav e2e: unchanged.

Draft: the root cause comes from the triage done for #4884 (A/B against 4.28.0 / 4.29.0 / 4.30 with the same app and only the library sources swapped), the patch builds (Debug) but has not been run against the scenario yet.

Checklist

  • Included code example that can be used to test this change (tabs/test-tabs-more-navigation-controller-ios).
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

🤖 Generated with Claude Code

…on while More is active

On the UITab path `applySelectedScreenController:` falls back to
`setSelectedViewController:` while the More tab is active. UIKit sends
`didSelectTab:` synchronously from inside that write and, since #4862,
an unflagged `didSelectTab:` counts as a user selection, so a programmatic
selection out of a More tab progressed the state twice and emitted a
second, `user`-origin `onTabSelected`. Wrap the fallback in
`_isApplyingProgrammaticTabSelection` like the other programmatic writes.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e7ee2971-04e5-4e53-a8b9-823331a81321
📥 Commits

Reviewing files that changed from the base of the PR and between 54d2702 and 9cf2fdf.

📒 Files selected for processing (1)
  • ios/tabs/host/RNSTabBarController.mm
 ________________________________________
< CI is red. I'm also red. We match now. >
 ----------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Tested the runtime, seems to fix the regression.d

@t0maboro
t0maboro marked this pull request as ready for review October 13, 2026 08:36
@t0maboro t0maboro mentioned this pull request Oct 13, 2026
12 of 13 tasks
@t0maboro t0maboro added the action:backport-to-v4 Add this label to any issue or PR that should be backported to the v4 line of the library. label Oct 13, 2026
@t0maboro
t0maboro merged commit 4a87a71 into main Oct 13, 2026
7 of 9 checks passed
@t0maboro
t0maboro deleted the @t0maboro/fix-more-programmatic-selection-duplicate branch October 13, 2026 08:40
t0maboro added a commit that referenced this pull request Oct 13, 2026
…on while More is active (#4890) (#4892)

(generated by Claude)

## Description

On the `UITab` API path (iOS 26.1+) a programmatic tab selection made
while the More tab is active emits `onTabSelected` twice: a
`user`-origin event first, then the `programmatic-js` one, and the
navigation state provenance skips a value. It happens when the current
tab overflows into More and was itself selected programmatically (e.g.
`Select Sixth` from JS, then `Select Second`). Entering More through the
More list and then selecting programmatically gives a single event.
Found during the 4.30.0 release testing (#4884), regression from #4862;
4.29.0 and 4.28.0 emit one event.

Root cause: `-[RNSTabBarController applySelectedScreenController:]` sets
`_isApplyingProgrammaticTabSelection` only around `self.selectedTab =
…`, the branch taken when the More item is NOT selected. While More is
selected it falls back to `[self setSelectedViewController:]` without
the flag. UIKit sends `tabBarController:didSelectTab:previousTab:`
synchronously from inside that write, and since #4862 every unflagged
`didSelectTab:` is treated as a user selection:
`userDidSelectViewController:` progresses the state once more and emits
a `user` event, after which the programmatic path emits its own event
with the already-bumped provenance. Before #4862 the filter worked the
other way round (flag set in `shouldSelectTab:`), so the programmatic
`setSelectedViewController:` write was ignored.

## Changes

- `RNSTabBarController -applySelectedScreenController:`:
`_isApplyingProgrammaticTabSelection` now wraps the whole `UITab`
branch, including the `setSelectedViewController:` fallback used while
More is active. Same pattern as `installScreenControllers:` around
`setTabs:` / `selectedTab`.

## Before & after - visual documentation

| Before | After |
| --- | --- |
| <video

src="https://github.com/user-attachments/assets/3336fc09-d892-4436-acd4-9b6d8fef900a"
/> | <video

src="https://github.com/user-attachments/assets/a3e3e738-3a59-4afb-bec5-d10ccdeac7b6"
/> |

## Test plan

- `tabs/test-tabs-more-navigation-controller-ios` (SFT), iPhone
simulator, iOS 27.0 and 26.5: `Select Sixth`, then `Select Second`.
Before: two `onTabSelected` toasts for `Second`; after: one, with the
provenance advanced by exactly one (check the `onTabSelected` payload).
Also More → Fifth → `Select First` (one event, unchanged) and user taps
between More and regular tabs (unchanged).
- `tabs-stack-v4/test-tabs-search-tab-activation-ios` on iOS 27.0, the
#4862 scenario: reselecting the search tab through the collapsed tab bar
still shows "Selected in JS: yes".
- `tabs/test-tabs-navigation-state-search-tab` and
`tabs/test-tabs-simple-nav` e2e: unchanged.

Draft: the root cause comes from the triage done for #4884 (A/B against
4.28.0 / 4.29.0 / 4.30 with the same app and only the library sources
swapped), the patch builds (Debug) but has not been run against the
scenario yet.

## Checklist

- [x] Included code example that can be used to test this change
(`tabs/test-tabs-more-navigation-controller-ios`).
- [ ] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [ ] For API changes, updated relevant public types.
- [ ] Ensured that CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <[email protected]>
(cherry picked from commit 4a87a71)

Co-authored-by: Claude Fable 5.1 <[email protected]>
t0maboro added a commit that referenced this pull request Oct 13, 2026
…on while More is active (#4890) (#4892) (#4894)

(generated by Claude)

## Description

On the `UITab` API path (iOS 26.1+) a programmatic tab selection made
while the More tab is active emits `onTabSelected` twice: a
`user`-origin event first, then the `programmatic-js` one, and the
navigation state provenance skips a value. It happens when the current
tab overflows into More and was itself selected programmatically (e.g.
`Select Sixth` from JS, then `Select Second`). Entering More through the
More list and then selecting programmatically gives a single event.
Found during the 4.30.0 release testing (#4884), regression from #4862;
4.29.0 and 4.28.0 emit one event.

Root cause: `-[RNSTabBarController applySelectedScreenController:]` sets
`_isApplyingProgrammaticTabSelection` only around `self.selectedTab =
…`, the branch taken when the More item is NOT selected. While More is
selected it falls back to `[self setSelectedViewController:]` without
the flag. UIKit sends `tabBarController:didSelectTab:previousTab:`
synchronously from inside that write, and since #4862 every unflagged
`didSelectTab:` is treated as a user selection:
`userDidSelectViewController:` progresses the state once more and emits
a `user` event, after which the programmatic path emits its own event
with the already-bumped provenance. Before #4862 the filter worked the
other way round (flag set in `shouldSelectTab:`), so the programmatic
`setSelectedViewController:` write was ignored.

## Changes

- `RNSTabBarController -applySelectedScreenController:`:
`_isApplyingProgrammaticTabSelection` now wraps the whole `UITab`
branch, including the `setSelectedViewController:` fallback used while
More is active. Same pattern as `installScreenControllers:` around
`setTabs:` / `selectedTab`.

## Before & after - visual documentation

| Before | After |
| --- | --- |
| <video


src="https://github.com/user-attachments/assets/3336fc09-d892-4436-acd4-9b6d8fef900a"
/> | <video


src="https://github.com/user-attachments/assets/a3e3e738-3a59-4afb-bec5-d10ccdeac7b6"
/> |

## Test plan

- `tabs/test-tabs-more-navigation-controller-ios` (SFT), iPhone
simulator, iOS 27.0 and 26.5: `Select Sixth`, then `Select Second`.
Before: two `onTabSelected` toasts for `Second`; after: one, with the
provenance advanced by exactly one (check the `onTabSelected` payload).
Also More → Fifth → `Select First` (one event, unchanged) and user taps
between More and regular tabs (unchanged).
- `tabs-stack-v4/test-tabs-search-tab-activation-ios` on iOS 27.0, the
#4862 scenario: reselecting the search tab through the collapsed tab bar
still shows "Selected in JS: yes".
- `tabs/test-tabs-navigation-state-search-tab` and
`tabs/test-tabs-simple-nav` e2e: unchanged.

Draft: the root cause comes from the triage done for #4884 (A/B against
4.28.0 / 4.29.0 / 4.30 with the same app and only the library sources
swapped), the patch builds (Debug) but has not been run against the
scenario yet.

## Checklist

- [x] Included code example that can be used to test this change
(`tabs/test-tabs-more-navigation-controller-ios`).
- [ ] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [ ] For API changes, updated relevant public types.
- [ ] Ensured that CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <[email protected]>
(cherry picked from commit 4a87a71)

Co-authored-by: Claude Fable 5.1 <[email protected]>
(cherry picked from commit 30c0cfe)

Co-authored-by: Claude Fable 5.1 <[email protected]>
@t0maboro t0maboro added backported-to-v4 PRs marked with `action:backport-to-v4` that were should replace label with this one and removed action:backport-to-v4 Add this label to any issue or PR that should be backported to the v4 line of the library. labels Oct 13, 2026
@t0maboro t0maboro mentioned this pull request Oct 13, 2026
structivist pushed a commit to sakiori/react-native-screens that referenced this pull request Nov 12, 2026
…on while More is active (software-mansion#4890)

(generated by Claude)

## Description

On the `UITab` API path (iOS 26.1+) a programmatic tab selection made
while the More tab is active emits `onTabSelected` twice: a
`user`-origin event first, then the `programmatic-js` one, and the
navigation state provenance skips a value. It happens when the current
tab overflows into More and was itself selected programmatically (e.g.
`Select Sixth` from JS, then `Select Second`). Entering More through the
More list and then selecting programmatically gives a single event.
Found during the 4.30.0 release testing (software-mansion#4884), regression from software-mansion#4862;
4.29.0 and 4.28.0 emit one event.

Root cause: `-[RNSTabBarController applySelectedScreenController:]` sets
`_isApplyingProgrammaticTabSelection` only around `self.selectedTab =
…`, the branch taken when the More item is NOT selected. While More is
selected it falls back to `[self setSelectedViewController:]` without
the flag. UIKit sends `tabBarController:didSelectTab:previousTab:`
synchronously from inside that write, and since software-mansion#4862 every unflagged
`didSelectTab:` is treated as a user selection:
`userDidSelectViewController:` progresses the state once more and emits
a `user` event, after which the programmatic path emits its own event
with the already-bumped provenance. Before software-mansion#4862 the filter worked the
other way round (flag set in `shouldSelectTab:`), so the programmatic
`setSelectedViewController:` write was ignored.

## Changes

- `RNSTabBarController -applySelectedScreenController:`:
`_isApplyingProgrammaticTabSelection` now wraps the whole `UITab`
branch, including the `setSelectedViewController:` fallback used while
More is active. Same pattern as `installScreenControllers:` around
`setTabs:` / `selectedTab`.

## Before & after - visual documentation

| Before | After |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/3336fc09-d892-4436-acd4-9b6d8fef900a"
/> | <video
src="https://github.com/user-attachments/assets/a3e3e738-3a59-4afb-bec5-d10ccdeac7b6"
/> |

## Test plan

- `tabs/test-tabs-more-navigation-controller-ios` (SFT), iPhone
simulator, iOS 27.0 and 26.5: `Select Sixth`, then `Select Second`.
Before: two `onTabSelected` toasts for `Second`; after: one, with the
provenance advanced by exactly one (check the `onTabSelected` payload).
Also More → Fifth → `Select First` (one event, unchanged) and user taps
between More and regular tabs (unchanged).
- `tabs-stack-v4/test-tabs-search-tab-activation-ios` on iOS 27.0, the
software-mansion#4862 scenario: reselecting the search tab through the collapsed tab bar
still shows "Selected in JS: yes".
- `tabs/test-tabs-navigation-state-search-tab` and
`tabs/test-tabs-simple-nav` e2e: unchanged.

Draft: the root cause comes from the triage done for software-mansion#4884 (A/B against
4.28.0 / 4.29.0 / 4.30 with the same app and only the library sources
swapped), the patch builds (Debug) but has not been run against the
scenario yet.

## Checklist

- [x] Included code example that can be used to test this change
(`tabs/test-tabs-more-navigation-controller-ios`).
- [ ] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [ ] For API changes, updated relevant public types.
- [ ] Ensured that CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <[email protected]>
(cherry picked from commit 4a87a71)
structivist pushed a commit to sakiori/react-native-screens that referenced this pull request Nov 12, 2026
Brings upstream's fix for user tab selections dropped by a mount
transaction (software-mansion#4862, software-mansion#4890) and the opt-in horizontal sheet bars from software-mansion#2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backported-to-v4 PRs marked with `action:backport-to-v4` that were should replace label with this one

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants