Skip to content

fix(iOS, Tabs): resolve content scroll view for tabBarMinimizeBehavior in real-world RN hierarchies#4153

Open
sgup wants to merge 1 commit into
software-mansion:mainfrom
sgup:fix/tab-bar-minimize-content-scroll-view
Open

fix(iOS, Tabs): resolve content scroll view for tabBarMinimizeBehavior in real-world RN hierarchies#4153
sgup wants to merge 1 commit into
software-mansion:mainfrom
sgup:fix/tab-bar-minimize-content-scroll-view

Conversation

@sgup

@sgup sgup commented Jun 11, 2026

Copy link
Copy Markdown

Note: The code / PR is made by Claude Fable, however I've manually tested and confirmed with my app on simulator and device.

Description

On iOS 26, tabBarMinimizeBehavior: 'onScrollDown' never engages when a tab's content is a nested (native stack) navigator and/or the scrolling content is a virtualized list (FlashList, LegendList) or any UIScrollView behind styled wrapper views — i.e. the common shape of a production app. The tab bar never minimizes and a bottom accessory never reaches its inline placement.

UIKit resolves the scroll view that drives minimize by asking the selected tab's view controller for its bottom-edge content scroll view (contentScrollView(for:) / automatic detection). UIKit's automatic detection cannot find a UIScrollView nested inside a React Native screen tree. The collapsable={false} workaround from #3954 only addresses the trivial single-styled-wrapper case — it cannot help when the first-descendant chain runs through a nested stack host or a virtualized list's wrappers.

Closes #4145.

Changes

  • RNSScrollViewFinder: new findScrollViewBreadthFirstFrom: — a bounded breadth-first search (2000-view visit cap) that skips horizontal-only scrollers (carousels), so behaviors driven by the result track the vertical list. The existing first-descendant-chain walk is exactly what fails on real screens.
  • RNSTabsScreenViewController + RNSScreen: implement contentScrollViewForEdge: (UIKit's documented override point), covering whichever controller UIKit queries (the tab root, or the visible nested-stack screen). UIKit's own resolution is tried first; the BFS is only a fallback, and only for the bottom edge so top-edge (navigation bar) resolution is unaffected.

Open questions for maintainers:

  • The RNSScreen override applies to all stack screens, not only tab-hosted ones. We saw no regressions (bottom edge only), but it could be gated to tab-hosted screens or behind a prop if preferred.
  • An alternative shape would be extending the existing RNSContentScrollViewProviding delegation so the BFS lives behind the provider protocol.

Test plan

Tested in a production Expo SDK 56 app (RN 0.85, new arch) on a physical iOS 26 device, with these changes applied to 4.25.2 via package patching (this area is unchanged between 4.25.2 and main):

  • Tabs each hosting a nested native stack whose screens render LegendList content behind styled wrappers/headers: scrolling down now minimizes the tab bar and moves the bottom accessory to its inline placement on every tab; scrolling up restores it. Previously minimize never engaged anywhere.
  • Screens with a horizontal carousel near the top: minimize still tracks the vertical list (carousel skipped by the finder).
  • Navigation-bar / top-edge behaviors unaffected (override restricted to the bottom edge).

Reproduction shape:

NativeTabs (tabBarMinimizeBehavior: onScrollDown)
└─ Tab
   └─ native stack navigator
      └─ screen
         └─ styled wrappers / headers
            └─ FlashList / LegendList / ScrollView

Same class of issue reported against react-native-bottom-tabs (callstack/react-native-bottom-tabs#496), where swizzling contentScrollView(for:) is the community fix; this PR uses the documented override point instead.

Checklist

  • Included code example that can be used to test this change.
  • For API changes, updated relevant public types. (No public API changes; new method is internal.)
  • Ensured that CI passes

…r in real-world RN hierarchies

On iOS 26, UIKit drives tabBarMinimizeBehavior (tab-bar minimize + inline
bottom accessory) from the selected tab view controller's bottom-edge
content scroll view. UIKit's automatic detection cannot find a UIScrollView
nested inside a typical React Native screen tree — styled wrapper views,
headers, virtualized lists (FlashList/LegendList), or a nested stack
navigator inside the tab — so minimize never engages. The
collapsable={false} workaround from software-mansion#3954 only addresses the trivial
single-wrapper case and cannot help with nested stacks or virtualized
lists.

Implement contentScrollViewForEdge: (UIKit's documented override point) on
RNSTabsScreenViewController and RNSScreen, covering whichever controller
UIKit queries: try UIKit's own resolution first, then fall back to a
breadth-first search of the attached content. Restricted to the bottom edge
so top-edge (navigation bar) resolution is unaffected.

The new RNSScrollViewFinder.findScrollViewBreadthFirstFrom: is a bounded BFS
that skips horizontal-only scrollers (carousels), so minimize tracks the
vertical list.

Fixes software-mansion#4145
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS 26] tabBarMinimizeBehavior never engages when tab content is a nested stack / virtualize list

1 participant