Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,25 @@ - (void)viewDidDisappear:(BOOL)animated
_shouldNotify = YES;
}

// See RNSTabsScreenViewController: when a tab hosts a nested stack, UIKit may
// resolve the bottom-edge content scroll view (which drives iOS 26
// `tabBarMinimizeBehavior`) against the visible stack screen's view
// controller rather than the tab root. UIKit's automatic detection cannot
// reach UIScrollViews nested in a React Native screen tree, so fall back to a
// breadth-first search. Restricted to the bottom edge so top-edge
// (navigation bar) resolution is unaffected.
- (UIScrollView *)contentScrollViewForEdge:(NSDirectionalRectEdge)edge
{
UIScrollView *scrollView = [super contentScrollViewForEdge:edge];
if (scrollView != nil) {
return scrollView;
}
if (edge == NSDirectionalRectEdgeBottom) {
return [RNSScrollViewFinder findScrollViewBreadthFirstFrom:self.view];
}
return nil;
}

- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
Expand Down
14 changes: 14 additions & 0 deletions ios/helpers/scroll-view/RNSScrollViewFinder.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@
*/
+ (nullable UIScrollView *)findContentScrollViewWithDelegatingToProvider:(nullable UIView *)view;

/**
* Breadth-first search for the first vertically-scrollable UIScrollView
* descendant. Unlike the first-descendant-chain walk, this finds scroll views
* in real-world screen trees (styled wrapper views, headers, virtualized
* lists such as FlashList/LegendList, nested stack navigators), which
* UIKit's automatic content-scroll-view detection cannot reach.
* Horizontal-only scrollers (e.g. carousels) are skipped, so behaviors driven
* by the result (such as `tabBarMinimizeBehavior`) track vertical scrolling.
* The search is bounded so a pathological tree cannot stall the main thread.
*
* When `view == nil`, it returns `nil`.
*/
+ (nullable UIScrollView *)findScrollViewBreadthFirstFrom:(nullable UIView *)view;

@end
33 changes: 33 additions & 0 deletions ios/helpers/scroll-view/RNSScrollViewFinder.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,37 @@ + (nullable UIScrollView *)findContentScrollViewWithDelegatingToProvider:(nullab
return nil;
}

+ (nullable UIScrollView *)findScrollViewBreadthFirstFrom:(nullable UIView *)view
{
if (view == nil) {
return nil;
}

static const NSUInteger kMaxVisitedViews = 2000;
NSMutableArray<UIView *> *queue = [NSMutableArray arrayWithObject:view];
NSUInteger index = 0;

while (index < queue.count && queue.count < kMaxVisitedViews) {
UIView *current = queue[index++];

if ([current isKindOfClass:UIScrollView.class]) {
UIScrollView *scrollView = static_cast<UIScrollView *>(current);
// Skip horizontal-only scrollers (carousels). Unmeasured scroll views
// (zero contentSize) pass, since the main list may not be laid out yet
// when UIKit first resolves the content scroll view.
BOOL isHorizontalOnly = scrollView.contentSize.width > scrollView.bounds.size.width &&
scrollView.contentSize.height <= scrollView.bounds.size.height;
if (!isHorizontalOnly) {
return scrollView;
}
// Do not descend into a skipped horizontal scroller.
continue;
}

[queue addObjectsFromArray:current.subviews];
}

return nil;
}

@end
20 changes: 20 additions & 0 deletions ios/tabs/screen/RNSTabsScreenViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ - (bool)tabScreenSelectedRepeatedly
return false;
}

// UIKit resolves the scroll view that drives `tabBarMinimizeBehavior`
// (iOS 26 tab-bar minimize + inline bottom accessory) by asking the selected
// tab's view controller for its bottom-edge content scroll view. UIKit's
// automatic detection cannot find UIScrollViews nested inside a typical
// React Native screen tree (styled wrappers, headers, virtualized lists,
// nested stack navigators), so minimize never engages. Fall back to a
// breadth-first search of the currently attached content. Restricted to the
// bottom edge so top-edge (navigation bar) resolution is unaffected.
- (UIScrollView *)contentScrollViewForEdge:(NSDirectionalRectEdge)edge
{
UIScrollView *scrollView = [super contentScrollViewForEdge:edge];
if (scrollView != nil) {
return scrollView;
}
if (edge == NSDirectionalRectEdgeBottom) {
return [RNSScrollViewFinder findScrollViewBreadthFirstFrom:self.view];
}
return nil;
}

#if !TARGET_OS_TV

- (RNSOrientation)evaluateOrientation
Expand Down