From 79a0ebfb5be99edded5ab66692bdb705868635da Mon Sep 17 00:00:00 2001 From: Marco de Jongh Date: Sat, 6 Jun 2026 19:47:39 +1000 Subject: [PATCH 1/3] feat(tabs): add iOS search toolbar items --- ios/RNSScreenStackHeaderConfig.mm | 6 + ios/tabs/host/RNSTabBarController.h | 15 +++ ios/tabs/host/RNSTabBarController.mm | 33 ++++++ ios/tabs/screen/RNSTabsScreenComponentView.h | 1 + ios/tabs/screen/RNSTabsScreenComponentView.mm | 18 +++ ios/tabs/screen/RNSTabsScreenEventEmitter.h | 1 + ios/tabs/screen/RNSTabsScreenEventEmitter.mm | 14 +++ ios/tabs/screen/RNSTabsScreenViewController.h | 11 ++ .../screen/RNSTabsScreenViewController.mm | 108 +++++++++++++++++- src/components/tabs/index.ts | 1 + src/components/tabs/screen/TabsScreen.ios.tsx | 28 +++++ .../tabs/screen/TabsScreen.ios.types.ts | 18 ++- .../tabs/TabsScreenIOSNativeComponent.ts | 5 + 13 files changed, 257 insertions(+), 2 deletions(-) diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 5dbe2e2e65..9573800ae7 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -22,6 +22,7 @@ #import "RNSDefines.h" #import "RNSScreen.h" #import "RNSSearchBar.h" +#import "RNSTabBarController.h" #import "UINavigationBar+RNSUtility.h" namespace react = facebook::react; @@ -627,6 +628,11 @@ + (void)updateViewController:(UIViewController *)vc if (!searchBarPresent) { navitem.searchController = nil; } + if ([vc.tabBarController isKindOfClass:RNSTabBarController.class]) { + RNSTabBarController *tabBarController = static_cast(vc.tabBarController); + tabBarController.needsUpdateOfSearchToolbarItems = true; + [tabBarController updateSearchToolbarItemsIfNeeded]; + } #endif /* !TARGET_OS_TV */ // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). diff --git a/ios/tabs/host/RNSTabBarController.h b/ios/tabs/host/RNSTabBarController.h index 5e89179c94..57ec8f29b1 100644 --- a/ios/tabs/host/RNSTabBarController.h +++ b/ios/tabs/host/RNSTabBarController.h @@ -226,6 +226,16 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)updateTabBarAppearance; +/** + * Updates native toolbar items associated with the selected search tab. + */ +- (void)updateSearchToolbarItemsIfNeeded; + +/** + * Updates native toolbar items associated with the selected search tab. + */ +- (void)updateSearchToolbarItems; + /** * Updates the interface orientation based on selected tab screen and its children. * @@ -287,6 +297,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readwrite) bool needsUpdateOfTabBarAppearance; +/** + * Tell the controller that toolbar items associated with the selected search tab need an update. + */ +@property (nonatomic, readwrite) bool needsUpdateOfSearchToolbarItems; + /** * Tell the controller that some configuration regarding interface orientation has changed & it * requires update. diff --git a/ios/tabs/host/RNSTabBarController.mm b/ios/tabs/host/RNSTabBarController.mm index 1379264e2a..277535780a 100644 --- a/ios/tabs/host/RNSTabBarController.mm +++ b/ios/tabs/host/RNSTabBarController.mm @@ -7,6 +7,7 @@ #import "NSString+RNSUtility.h" #import "RNSLog.h" #import "RNSScreenWindowTraits.h" +#import "RNSTabsHostComponentView+RNSImageLoader.h" #import "RNSTabsHostComponentView.h" #import "RNSTabsNavigationStateObserverRegistry.h" @@ -97,6 +98,7 @@ - (instancetype)init _navigationState = nil; _pendingStateUpdate = nil; _shouldProgressStateOnMoreNavigationControllerPush = NO; + _needsUpdateOfSearchToolbarItems = false; _observerRegistry = [RNSTabsNavigationStateObserverRegistry new]; // Delegate field retains weakly, no risk of cycle. @@ -250,6 +252,7 @@ - (void)performContainerUpdate _isHandlingExplicitSelectionUpdate = NO; [self updateTabBarAppearanceIfNeeded]; + [self updateSearchToolbarItemsIfNeeded]; [self updateTabBarA11yIfNeeded]; [self updateOrientationIfNeeded]; } @@ -280,10 +283,12 @@ - (BOOL)updateSelectedViewControllerTo:(nullable UIViewController *)nextSelected [self progressNavigationState:screenKey withOrigin:actionOrigin]; if (currSelectedViewController == nextSelectedViewController) { + self.needsUpdateOfSearchToolbarItems = true; return YES; } [self setSelectedViewController:nextSelectedViewController]; + self.needsUpdateOfSearchToolbarItems = true; return YES; } @@ -342,6 +347,8 @@ - (void)userDidSelectViewController:(nonnull UIViewController *)viewController actionOrigin:RNSTabsActionOriginUser]; [_observerRegistry emitDidUpdateStateTo:_navigationState withContext:updateContext sender:self]; } + self.needsUpdateOfSearchToolbarItems = true; + [self updateSearchToolbarItemsIfNeeded]; } - (void)onDidPreventUserFromSelectingViewControllerWithKey:(nonnull NSString *)screenKey @@ -582,6 +589,32 @@ - (void)updateTabBarAppearance imageLoader:[self.tabsHostComponentView reactImageLoader]]; } +- (void)updateSearchToolbarItemsIfNeeded +{ + if (_needsUpdateOfSearchToolbarItems) { + [self updateSearchToolbarItems]; + } +} + +- (void)updateSearchToolbarItems +{ + _needsUpdateOfSearchToolbarItems = false; + + RNSTabsScreenViewController *selectedScreenViewController = nil; + if (![self isSelectedViewControllerTheMoreNavigationController] && + [self.selectedViewController isKindOfClass:RNSTabsScreenViewController.class]) { + selectedScreenViewController = [self selectedScreenViewController]; + } + + for (RNSTabsScreenViewController *screenViewController in _tabScreenControllers) { + if (screenViewController != selectedScreenViewController) { + [screenViewController clearSearchToolbarItems]; + } + } + + [selectedScreenViewController updateSearchToolbarItemsWithImageLoader:[self.tabsHostComponentView reactImageLoader]]; +} + - (void)updateTabBarA11yIfNeeded { for (UIViewController *tabViewController in self.viewControllers) { diff --git a/ios/tabs/screen/RNSTabsScreenComponentView.h b/ios/tabs/screen/RNSTabsScreenComponentView.h index 38805843e6..08d3585820 100644 --- a/ios/tabs/screen/RNSTabsScreenComponentView.h +++ b/ios/tabs/screen/RNSTabsScreenComponentView.h @@ -70,6 +70,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL tabBarItemNeedsA11yUpdate; @property (nonatomic, readonly) RNSTabsScreenSystemItem systemItem; +@property (nonatomic, copy, readonly, nullable) NSArray *> *searchToolbarItems; @end diff --git a/ios/tabs/screen/RNSTabsScreenComponentView.mm b/ios/tabs/screen/RNSTabsScreenComponentView.mm index 4c3e65a864..485f08ba38 100644 --- a/ios/tabs/screen/RNSTabsScreenComponentView.mm +++ b/ios/tabs/screen/RNSTabsScreenComponentView.mm @@ -1,6 +1,7 @@ #import "RNSTabsScreenComponentView.h" #import "NSString+RNSUtility.h" #import "RNSConversions.h" +#import "RNSConvert.h" #import "RNSDefines.h" #import "RNSLog.h" #import "RNSSafeAreaViewNotifications.h" @@ -80,6 +81,7 @@ - (void)resetProps _selectedIconResourceName = nil; _systemItem = RNSTabsScreenSystemItemNone; + _searchToolbarItems = nil; _userInterfaceStyle = UIUserInterfaceStyleUnspecified; } @@ -338,6 +340,22 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props _systemItem = rnscreens::conversion::RNSTabsScreenSystemItemFromReactRNSTabsScreenSystemItem(newComponentProps.systemItem); tabBarItemNeedsRecreation = YES; + RNSTabBarController *tabBarController = [self findTabBarController]; + tabBarController.needsUpdateOfSearchToolbarItems = true; + } + + if (newComponentProps.toolbarItems != oldComponentProps.toolbarItems) { + const auto &vec = newComponentProps.toolbarItems; + NSMutableArray *> *array = [NSMutableArray arrayWithCapacity:vec.size()]; + for (const auto &item : vec) { + NSDictionary *dict = [RNSConvert idFromFollyDynamic:item]; + if ([dict isKindOfClass:NSDictionary.class]) { + [array addObject:dict]; + } + } + _searchToolbarItems = array; + RNSTabBarController *tabBarController = [self findTabBarController]; + tabBarController.needsUpdateOfSearchToolbarItems = true; } if (newComponentProps.userInterfaceStyle != oldComponentProps.userInterfaceStyle) { diff --git a/ios/tabs/screen/RNSTabsScreenEventEmitter.h b/ios/tabs/screen/RNSTabsScreenEventEmitter.h index b2462dd13e..e15b9f571d 100644 --- a/ios/tabs/screen/RNSTabsScreenEventEmitter.h +++ b/ios/tabs/screen/RNSTabsScreenEventEmitter.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)emitOnDidAppear; - (BOOL)emitOnWillDisappear; - (BOOL)emitOnDidDisappear; +- (BOOL)emitOnPressToolbarItem:(NSString *)buttonId; @end diff --git a/ios/tabs/screen/RNSTabsScreenEventEmitter.mm b/ios/tabs/screen/RNSTabsScreenEventEmitter.mm index cdfebe392c..de15ce84c0 100644 --- a/ios/tabs/screen/RNSTabsScreenEventEmitter.mm +++ b/ios/tabs/screen/RNSTabsScreenEventEmitter.mm @@ -4,6 +4,8 @@ #import #import +namespace react = facebook::react; + @implementation RNSTabsScreenEventEmitter { std::shared_ptr _reactEventEmitter; } @@ -52,6 +54,18 @@ - (BOOL)emitOnDidDisappear } } +- (BOOL)emitOnPressToolbarItem:(NSString *)buttonId +{ + if (_reactEventEmitter != nullptr) { + _reactEventEmitter->onPressToolbarItem( + react::RNSTabsScreenIOSEventEmitter::OnPressToolbarItem{.buttonId = std::string([buttonId UTF8String])}); + return YES; + } else { + RCTLogWarn(@"[RNScreens] Skipped OnPressToolbarItem event emission due to nullish emitter"); + return NO; + } +} + - (void)updateEventEmitter:(const std::shared_ptr &)emitter { _reactEventEmitter = emitter; diff --git a/ios/tabs/screen/RNSTabsScreenViewController.h b/ios/tabs/screen/RNSTabsScreenViewController.h index 40d0985960..5227145c40 100644 --- a/ios/tabs/screen/RNSTabsScreenViewController.h +++ b/ios/tabs/screen/RNSTabsScreenViewController.h @@ -1,5 +1,6 @@ #pragma once +#import #import #import "RNSTabsScreenComponentView.h" #import "RNSTabsSpecialEffectsSupporting.h" @@ -28,6 +29,16 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)tabScreenOrientationHasChanged; +/** + * Updates native toolbar items associated with this tab screen's search item. + */ +- (void)updateSearchToolbarItemsWithImageLoader:(nullable RCTImageLoader *)imageLoader; + +/** + * Restores toolbar items previously replaced by this tab screen. + */ +- (void)clearSearchToolbarItems; + /** * Tell the controller that the tab item related to this controller has been selected again after being presented. * Returns boolean indicating whether the action has been handled. diff --git a/ios/tabs/screen/RNSTabsScreenViewController.mm b/ios/tabs/screen/RNSTabsScreenViewController.mm index 6bfece3edd..06e2840b69 100644 --- a/ios/tabs/screen/RNSTabsScreenViewController.mm +++ b/ios/tabs/screen/RNSTabsScreenViewController.mm @@ -1,10 +1,15 @@ #import "RNSTabsScreenViewController.h" +#import "RNSBarButtonItem.h" +#import "RNSDefines.h" #import "RNSLog.h" #import "RNSScrollViewFinder.h" #import "RNSTabBarController.h" #import "UIScrollView+RNScreens.h" -@implementation RNSTabsScreenViewController +@implementation RNSTabsScreenViewController { + __weak UIViewController *_searchToolbarItemsOwner; + NSArray *_searchToolbarItemsOwnerBaseItems; +} - (nullable RNSTabBarController *)findTabBarController { @@ -26,6 +31,104 @@ - (void)tabScreenOrientationHasChanged [[self findTabBarController] setNeedsOrientationUpdate:true]; } +- (void)clearSearchToolbarItems +{ +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV + if (@available(iOS 26.0, *)) { + if (_searchToolbarItemsOwner != nil) { + [_searchToolbarItemsOwner setToolbarItems:_searchToolbarItemsOwnerBaseItems animated:YES]; + _searchToolbarItemsOwner = nil; + _searchToolbarItemsOwnerBaseItems = nil; + } + } +#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV +} + +- (void)updateSearchToolbarItemsWithImageLoader:(nullable RCTImageLoader *)imageLoader +{ +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV + if (@available(iOS 26.0, *)) { + RNSTabsScreenComponentView *screenView = self.tabScreenComponentView; + UIViewController *toolbarOwner = [self activeSearchToolbarViewController]; + UINavigationItem *navigationItem = toolbarOwner.navigationItem; + UIBarButtonItem *searchBarItem = navigationItem.searchBarPlacementBarButtonItem; + + if (screenView.systemItem != RNSTabsScreenSystemItemSearch || screenView.searchToolbarItems.count == 0 || + navigationItem.searchController == nil || !navigationItem.searchBarPlacementAllowsToolbarIntegration || + searchBarItem == nil) { + [self clearSearchToolbarItems]; + return; + } + + NSArray *baseItems = nil; + if (_searchToolbarItemsOwner == toolbarOwner) { + baseItems = _searchToolbarItemsOwnerBaseItems ?: @[]; + } else { + [self clearSearchToolbarItems]; + _searchToolbarItemsOwner = toolbarOwner; + _searchToolbarItemsOwnerBaseItems = toolbarOwner.toolbarItems ?: @[]; + baseItems = _searchToolbarItemsOwnerBaseItems; + } + + NSMutableArray *items = [NSMutableArray arrayWithArray:baseItems ?: @[]]; + [items addObject:searchBarItem]; + [items addObjectsFromArray:[self toolbarButtonItemsFromConfigs:screenView.searchToolbarItems + imageLoader:imageLoader]]; + [toolbarOwner setToolbarItems:items animated:YES]; + } +#else + [self clearSearchToolbarItems]; +#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV +} + +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV +- (UIViewController *)activeSearchToolbarViewController API_AVAILABLE(ios(26.0)) +{ + UIViewController *controller = self; + while (true) { + if ([controller isKindOfClass:UINavigationController.class]) { + UINavigationController *navigationController = static_cast(controller); + UIViewController *visibleViewController = + navigationController.visibleViewController ?: navigationController.topViewController; + if (visibleViewController == nil || visibleViewController == controller) { + return controller; + } + controller = visibleViewController; + continue; + } + + UIViewController *childViewController = controller.childViewControllers.lastObject; + if (childViewController == nil) { + return controller; + } + controller = childViewController; + } +} + +- (NSArray *)toolbarButtonItemsFromConfigs:(NSArray *> *)dicts + imageLoader:(nullable RCTImageLoader *)imageLoader + API_AVAILABLE(ios(26.0)) +{ + NSMutableArray *items = [NSMutableArray arrayWithCapacity:dicts.count]; + __weak RNSTabsScreenViewController *weakSelf = self; + for (NSDictionary *dict in dicts) { + if (dict[@"buttonId"] == nil) { + continue; + } + + RNSBarButtonItem *item = [[RNSBarButtonItem alloc] + initWithConfig:dict + action:^(NSString *buttonId) { + [weakSelf.tabScreenComponentView.reactEventEmitter emitOnPressToolbarItem:buttonId]; + } + menuAction:nil + imageLoader:imageLoader]; + [items addObject:item]; + } + return items; +} +#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV + - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; @@ -36,6 +139,9 @@ - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.tabScreenComponentView.reactEventEmitter emitOnDidAppear]; + RNSTabBarController *tabBarController = [self findTabBarController]; + tabBarController.needsUpdateOfSearchToolbarItems = true; + [tabBarController updateSearchToolbarItemsIfNeeded]; } - (void)viewWillDisappear:(BOOL)animated diff --git a/src/components/tabs/index.ts b/src/components/tabs/index.ts index 982e073fd2..b1351e00f3 100644 --- a/src/components/tabs/index.ts +++ b/src/components/tabs/index.ts @@ -34,6 +34,7 @@ export type { // iOS TabsScreenBlurEffect, TabsScreenSystemItem, + TabsScreenToolbarItemIOS, TabsScreenAppearanceIOS, TabsScreenItemAppearanceIOS, TabsScreenItemStateAppearanceIOS, diff --git a/src/components/tabs/screen/TabsScreen.ios.tsx b/src/components/tabs/screen/TabsScreen.ios.tsx index 6a136032bc..7f1a60682c 100644 --- a/src/components/tabs/screen/TabsScreen.ios.tsx +++ b/src/components/tabs/screen/TabsScreen.ios.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { ImageResolvedAssetSource, + NativeSyntheticEvent, StyleSheet, processColor, type ImageSourcePropType, @@ -21,6 +22,7 @@ import type { } from './TabsScreen.ios.types'; import type { TabsScreenProps } from './TabsScreen.types'; import type { PlatformIconIOS } from '../../../types'; +import { prepareHeaderBarButtonItems } from '../../helpers/prepareHeaderBarButtonItems'; import { useTabsScreen } from './useTabsScreen'; /** @@ -55,6 +57,30 @@ function TabsScreen(props: TabsScreenProps) { }); const iconProps = parseIconsToNativeProps(ios?.icon, ios?.selectedIcon); + const toolbarItems = React.useMemo( + () => + ios?.toolbarItems + ? prepareHeaderBarButtonItems(ios.toolbarItems, 'right') + : undefined, + [ios?.toolbarItems], + ); + const onPressToolbarItem = toolbarItems + ? (event: NativeSyntheticEvent<{ buttonId: string }>) => { + const pressedItem = toolbarItems.find( + item => + item && + 'buttonId' in item && + item.buttonId === event.nativeEvent.buttonId, + ); + if ( + pressedItem && + pressedItem.type === 'button' && + pressedItem.onPress + ) { + pressedItem.onPress(); + } + } + : undefined; return ( diff --git a/src/components/tabs/screen/TabsScreen.ios.types.ts b/src/components/tabs/screen/TabsScreen.ios.types.ts index 58b35b9265..0f337e5086 100644 --- a/src/components/tabs/screen/TabsScreen.ios.types.ts +++ b/src/components/tabs/screen/TabsScreen.ios.types.ts @@ -1,6 +1,9 @@ import type { ColorValue, TextStyle } from 'react-native'; import type { UserInterfaceStyle, BlurEffect } from '../../shared/types'; -import type { PlatformIconIOS } from '../../../types'; +import type { + HeaderBarButtonItemWithAction, + PlatformIconIOS, +} from '../../../types'; export type TabsScreenBlurEffect = BlurEffect | 'systemDefault'; @@ -18,6 +21,8 @@ export type TabsScreenSystemItem = | 'search' | 'topRated'; +export type TabsScreenToolbarItemIOS = HeaderBarButtonItemWithAction; + export interface TabsScreenAppearanceIOS { /** * @summary Specifies the appearance of tab bar items when they are in stacked layout. @@ -277,6 +282,17 @@ export interface TabsScreenPropsIOS { * @platform ios */ systemItem?: TabsScreenSystemItem | undefined; + /** + * @summary Native toolbar items displayed next to the integrated search item. + * + * On iOS 26 and later, when this screen uses `systemItem: 'search'` and the active + * nested screen has a search controller integrated into the toolbar, these items are + * appended after `UINavigationItem.searchBarPlacementBarButtonItem`. + * + * @platform ios + * @supported iOS 26 or higher + */ + toolbarItems?: TabsScreenToolbarItemIOS[] | undefined; /** * @summary Specifies if `contentInsetAdjustmentBehavior` of first ScrollView * in first descendant chain from tab screen should be overridden back from `never` diff --git a/src/fabric/tabs/TabsScreenIOSNativeComponent.ts b/src/fabric/tabs/TabsScreenIOSNativeComponent.ts index 26f587ef66..ad9a7c416b 100644 --- a/src/fabric/tabs/TabsScreenIOSNativeComponent.ts +++ b/src/fabric/tabs/TabsScreenIOSNativeComponent.ts @@ -14,6 +14,7 @@ import { UnsafeMixed } from './codegenUtils'; // eslint-disable-next-line @typescript-eslint/ban-types type GenericEmptyEvent = Readonly<{}>; +type OnPressToolbarItemEvent = Readonly<{ buttonId: string }>; // #endregion General helpers @@ -115,6 +116,9 @@ export interface NativeProps extends ViewProps { onDidAppear?: CT.DirectEventHandler | undefined; onWillDisappear?: CT.DirectEventHandler | undefined; onDidDisappear?: CT.DirectEventHandler | undefined; + onPressToolbarItem?: + | CT.DirectEventHandler + | undefined; // Control screenKey: string; @@ -145,6 +149,7 @@ export interface NativeProps extends ViewProps { // Tab config isTitleUndefined?: CT.WithDefault; systemItem?: CT.WithDefault; + toolbarItems?: CT.UnsafeMixed[] | undefined; // Appearance standardAppearance?: UnsafeMixed | undefined; From b51d8a459a1c71b9aa58b347ab87f6d0a4ac2147 Mon Sep 17 00:00:00 2001 From: Marco de Jongh Date: Sat, 6 Jun 2026 21:05:33 +1000 Subject: [PATCH 2/3] fix(tabs): show iOS search toolbar items --- apps/src/tests/issue-tests/Test3168.tsx | 19 +++++++++++++++++++ .../screen/RNSTabsScreenViewController.mm | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/apps/src/tests/issue-tests/Test3168.tsx b/apps/src/tests/issue-tests/Test3168.tsx index dacef64e70..1e245313ea 100644 --- a/apps/src/tests/issue-tests/Test3168.tsx +++ b/apps/src/tests/issue-tests/Test3168.tsx @@ -197,6 +197,7 @@ function TabsStackComponent() { const [config, setConfig] = React.useState( DEFAULT_GLOBAL_CONFIGURATION, ); + const [activeFilterCount, setActiveFilterCount] = useState(2); const { searchBarConfig } = useSearchBarConfig(); const TAB_CONFIGS: TabRouteConfig[] = [ @@ -237,6 +238,24 @@ function TabsStackComponent() { name: 'magnifyingglass', }, systemItem: searchBarConfig.useSystemItem ? 'search' : undefined, + toolbarItems: [ + { + type: 'button', + icon: { + type: 'sfSymbol', + name: 'line.3.horizontal.decrease', + }, + accessibilityLabel: 'Filters', + accessibilityHint: 'Updates active filters count', + badge: { + value: String(activeFilterCount), + }, + onPress: () => + setActiveFilterCount(currentFilterCount => + currentFilterCount + 1, + ), + }, + ], }, }, }, diff --git a/ios/tabs/screen/RNSTabsScreenViewController.mm b/ios/tabs/screen/RNSTabsScreenViewController.mm index 06e2840b69..1ae0137630 100644 --- a/ios/tabs/screen/RNSTabsScreenViewController.mm +++ b/ios/tabs/screen/RNSTabsScreenViewController.mm @@ -9,6 +9,7 @@ @implementation RNSTabsScreenViewController { __weak UIViewController *_searchToolbarItemsOwner; NSArray *_searchToolbarItemsOwnerBaseItems; + BOOL _searchToolbarItemsOwnerHadHiddenToolbar; } - (nullable RNSTabBarController *)findTabBarController @@ -37,8 +38,11 @@ - (void)clearSearchToolbarItems if (@available(iOS 26.0, *)) { if (_searchToolbarItemsOwner != nil) { [_searchToolbarItemsOwner setToolbarItems:_searchToolbarItemsOwnerBaseItems animated:YES]; + [_searchToolbarItemsOwner.navigationController setToolbarHidden:_searchToolbarItemsOwnerHadHiddenToolbar + animated:YES]; _searchToolbarItemsOwner = nil; _searchToolbarItemsOwnerBaseItems = nil; + _searchToolbarItemsOwnerHadHiddenToolbar = NO; } } #endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV @@ -67,6 +71,7 @@ - (void)updateSearchToolbarItemsWithImageLoader:(nullable RCTImageLoader *)image [self clearSearchToolbarItems]; _searchToolbarItemsOwner = toolbarOwner; _searchToolbarItemsOwnerBaseItems = toolbarOwner.toolbarItems ?: @[]; + _searchToolbarItemsOwnerHadHiddenToolbar = toolbarOwner.navigationController.toolbarHidden; baseItems = _searchToolbarItemsOwnerBaseItems; } @@ -75,6 +80,7 @@ - (void)updateSearchToolbarItemsWithImageLoader:(nullable RCTImageLoader *)image [items addObjectsFromArray:[self toolbarButtonItemsFromConfigs:screenView.searchToolbarItems imageLoader:imageLoader]]; [toolbarOwner setToolbarItems:items animated:YES]; + [toolbarOwner.navigationController setToolbarHidden:NO animated:YES]; } #else [self clearSearchToolbarItems]; From 51656a4319b1f4768f4bf30d9534b69ef15b7cfe Mon Sep 17 00:00:00 2001 From: Marco de Jongh Date: Sat, 6 Jun 2026 21:43:20 +1000 Subject: [PATCH 3/3] fix(tabs): align iOS search toolbar items --- .../screen/RNSTabsScreenViewController.mm | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ios/tabs/screen/RNSTabsScreenViewController.mm b/ios/tabs/screen/RNSTabsScreenViewController.mm index 1ae0137630..d2a657afb6 100644 --- a/ios/tabs/screen/RNSTabsScreenViewController.mm +++ b/ios/tabs/screen/RNSTabsScreenViewController.mm @@ -6,6 +6,10 @@ #import "RNSTabBarController.h" #import "UIScrollView+RNScreens.h" +static constexpr CGFloat RNSSearchToolbarDefaultLeadingReservedWidth = 80.0; +static constexpr CGFloat RNSSearchToolbarLeadingReservedPadding = 4.0; +static constexpr CGFloat RNSSearchToolbarMaxCompactTabControlWidth = 72.0; + @implementation RNSTabsScreenViewController { __weak UIViewController *_searchToolbarItemsOwner; NSArray *_searchToolbarItemsOwnerBaseItems; @@ -76,6 +80,10 @@ - (void)updateSearchToolbarItemsWithImageLoader:(nullable RCTImageLoader *)image } NSMutableArray *items = [NSMutableArray arrayWithArray:baseItems ?: @[]]; + CGFloat leadingReservedWidth = [self searchToolbarLeadingReservedWidth]; + if (leadingReservedWidth > 0.0) { + [items addObject:[UIBarButtonItem fixedSpaceItemOfWidth:leadingReservedWidth]]; + } [items addObject:searchBarItem]; [items addObjectsFromArray:[self toolbarButtonItemsFromConfigs:screenView.searchToolbarItems imageLoader:imageLoader]]; @@ -88,6 +96,33 @@ - (void)updateSearchToolbarItemsWithImageLoader:(nullable RCTImageLoader *)image } #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV +- (CGFloat)searchToolbarLeadingReservedWidth API_AVAILABLE(ios(26.0)) +{ + UITabBar *tabBar = self.tabBarController.tabBar; + if (tabBar == nil || CGRectIsEmpty(tabBar.bounds)) { + return self.tabBarController.selectedIndex == 0 ? 0.0 : RNSSearchToolbarDefaultLeadingReservedWidth; + } + + CGFloat tabBarMidX = CGRectGetMidX(tabBar.bounds); + CGFloat leadingMaxX = 0.0; + for (UIView *subview in tabBar.subviews) { + CGRect frame = subview.frame; + CGFloat width = CGRectGetWidth(frame); + if (subview.hidden || subview.alpha < 0.01 || CGRectIsEmpty(frame) || + width > RNSSearchToolbarMaxCompactTabControlWidth || CGRectGetMidX(frame) >= tabBarMidX) { + continue; + } + + leadingMaxX = MAX(leadingMaxX, CGRectGetMaxX(frame)); + } + + if (leadingMaxX > 0.0) { + return leadingMaxX + RNSSearchToolbarLeadingReservedPadding; + } + + return self.tabBarController.selectedIndex == 0 ? 0.0 : RNSSearchToolbarDefaultLeadingReservedWidth; +} + - (UIViewController *)activeSearchToolbarViewController API_AVAILABLE(ios(26.0)) { UIViewController *controller = self;