Skip to content

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

@sgup

Description

@sgup

Description

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

#3954 covered a narrow variant of this (scroll view wrapped in a single styled View) and was closed with the collapsable={false} workaround. That workaround can't help here: with a nested stack the first-descendant chain from the TabsScreen runs through the stack host and screen views, and with virtualized lists + headers the UIScrollView is simply not on the first-subview chain at all — there is nothing the app author can collapsable={false} their way out of.

Mechanism

UIKit resolves the scroll view that drives the minimize behavior 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, so it tracks nothing and UITabBarController.tabBarMinimizeBehavior is inert.

Reproduction shape

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

RNS 4.25.2, RN 0.85, iOS 26, new arch. (Same report exists for react-native-bottom-tabs: callstack/react-native-bottom-tabs#496, where swizzling contentScrollView(for:) is the known fix.)

Proposed fix (running in production via a patch)

Implement contentScrollViewForEdge: — UIKit's documented override point for exactly this — on RNSTabsScreenViewController and RNSScreen, falling back to a breadth-first search when UIKit's own resolution returns nil. Restricted to the bottom edge so top-edge/navigation-bar behaviors are untouched:

- (UIScrollView *)contentScrollViewForEdge:(NSDirectionalRectEdge)edge
{
  UIScrollView *scrollView = [super contentScrollViewForEdge:edge];
  if (scrollView != nil) {
    return scrollView;
  }
  if (edge == NSDirectionalRectEdgeBottom) {
    return [RNSScrollViewFinder findScrollViewBreadthFirstFrom:self.view];
  }
  return nil;
}

plus a new finder on RNSScrollViewFinder (the existing first-descendant-chain walk is what fails on real screens):

// Bounded BFS; skips horizontal-only scrollers (carousels) so the tab bar
// tracks vertical scrolling. ~2000-view visit cap as a safety bound.
+ (nullable UIScrollView *)findScrollViewBreadthFirstFrom:(nullable UIView *)view;

With both overrides in place (covering whichever VC UIKit queries — tab root or the visible nested-stack screen), minimize + inline accessory work with nested stacks and LegendList/FlashList content, verified on a physical iOS 26 device.

Notes / open questions for maintainers:

  • The RNSScreen override applies to all stack screens, not only those inside tabs. We saw no regressions (bottom-edge only), but you may prefer gating it to tab-hosted screens or behind a prop.
  • An alternative shape would be extending the RNSContentScrollViewProviding delegation so the BFS lives behind the existing provider protocol.

Happy to open a PR with this.

Metadata

Metadata

Assignees

Labels

missing-infoThe user didn't precise the problem enoughmissing-reproThis issue need minimum repro scenarioplatform:iosIssue related to iOS part of the library

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions