From 7a5fc2271ae1d8509902143a6bdbde1affadf29a Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 21 Oct 2026 13:36:43 +0100 Subject: [PATCH] fix(ios): match tab bar items to tabs by identity When built with the iOS 27 SDK, UITabBar.items follows display order and a search role tab moves to the end, while UITabBarController.tabs keeps declaration order. Matching items to routes by position shifted titles, selected images and accessibility labels for every tab after the search tab. Fixes #596 Co-Authored-By: Claude Opus 5.5 --- .changeset/ios27-tab-item-order.md | 5 +++++ .../ios/TabViewImpl.swift | 22 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 .changeset/ios27-tab-item-order.md diff --git a/.changeset/ios27-tab-item-order.md b/.changeset/ios27-tab-item-order.md new file mode 100644 index 00000000..8db306d0 --- /dev/null +++ b/.changeset/ios27-tab-item-order.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': patch +--- + +Fix shifted tab titles, selected icons and accessibility labels on iOS when built with the iOS 27 SDK and a tab uses the `search` role diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index ad493310..6de510d0 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -120,14 +120,27 @@ struct TabViewImpl: View { let items = tabBar.items else { return } - configureTabBarItemImages(items: items, props: props) + configureTabBarItemImages(items: items, tabBar: tabBar, props: props) DispatchQueue.main.async { [weak tabBar] in guard let tabBar, let items = tabBar.items else { return } - configureTabBarItemImages(items: items, props: props) + configureTabBarItemImages(items: items, tabBar: tabBar, props: props) } } + /// Maps a tab bar item to its index in `filteredItems`. When linked against the iOS 27 SDK, + /// `UITabBar.items` follows display order (a `search` role tab moves to the end), while `tabs` + /// keeps declaration order. + private func declarationIndex(of item: UITabBarItem, at tabBarIndex: Int, in tabBar: UITabBar) -> Int { + if #available(iOS 18.0, tvOS 18.0, visionOS 2.0, *), + let tabController = tabBar.delegate as? UITabBarController, + let index = tabController.tabs.firstIndex(where: { $0.viewController?.tabBarItem === item }) + { + return index + } + return tabBarIndex + } + private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { guard let tabBar else { return } @@ -225,9 +238,10 @@ struct TabViewImpl: View { /// so it's never assigned here. Assigning item images while the tab bar animates (e.g. minimizing or /// switching tabs) breaks its selection styling on iOS 27, so `selectedImage` is only assigned when it /// changes, and SwiftUI updates its images only when a label changes. - private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) { + private func configureTabBarItemImages(items: [UITabBarItem], tabBar: UITabBar, props: TabViewProps) { for (tabBarIndex, item) in items.enumerated() { - guard let tabData = props.filteredItems[safe: tabBarIndex] else { continue } + let tabIndex = declarationIndex(of: item, at: tabBarIndex, in: tabBar) + guard let tabData = props.filteredItems[safe: tabIndex] else { continue } let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor let images = props.itemImages.images(for: tabData, props: props)