From 62c0b5a3451b1e046d0c833f3e6408343443a06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 27 Aug 2025 20:40:58 +0200 Subject: [PATCH] fix: improve subview foreach, use view as identifier --- .changeset/major-sites-end.md | 5 +++++ .../ios/TabBarFontSize.swift | 4 ++-- .../ios/TabItemEventModifier.swift | 6 +++--- .../ios/TabView/LegacyTabView.swift | 8 +++++--- .../ios/TabView/NewTabView.swift | 9 ++++----- .../react-native-bottom-tabs/ios/TabViewImpl.swift | 1 - .../react-native-bottom-tabs/ios/TabViewProps.swift | 13 +++++++++++-- .../ios/TabViewProvider.swift | 8 ++++---- 8 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 .changeset/major-sites-end.md diff --git a/.changeset/major-sites-end.md b/.changeset/major-sites-end.md new file mode 100644 index 0000000..a4f70ce --- /dev/null +++ b/.changeset/major-sites-end.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': patch +--- + +feat: improve subview foreach on iOS diff --git a/packages/react-native-bottom-tabs/ios/TabBarFontSize.swift b/packages/react-native-bottom-tabs/ios/TabBarFontSize.swift index b886042..48d9357 100644 --- a/packages/react-native-bottom-tabs/ios/TabBarFontSize.swift +++ b/packages/react-native-bottom-tabs/ios/TabBarFontSize.swift @@ -1,5 +1,5 @@ -import UIKit import React +import UIKit enum TabBarFontSize { /// Returns the default font size for tab bar item labels based on the current platform @@ -51,7 +51,7 @@ enum TabBarFontSize { } // Add color if provided - if let color = color { + if let color { attributes[.foregroundColor] = color } diff --git a/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift b/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift index 322a56c..21d1782 100644 --- a/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift +++ b/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift @@ -22,10 +22,10 @@ private final class TabBarDelegate: NSObject, UITabBarControllerDelegate { // Due to this, whether the tab prevents default has to be defined statically. if let index = tabBarController.viewControllers?.firstIndex(of: viewController) { let defaultPrevented = onClick?(index) ?? false - + return !defaultPrevented } - + return false } } @@ -60,7 +60,7 @@ struct TabItemEventModifier: ViewModifier { } // Create gesture handler - let handler = LongPressGestureHandler(tabBar: tabController.tabBar, handler: { key, isLongPress in _ = onTabEvent(key,isLongPress) }) + let handler = LongPressGestureHandler(tabBar: tabController.tabBar) { key, isLongPress in _ = onTabEvent(key, isLongPress) } let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:))) gesture.minimumPressDuration = 0.5 diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 6464986..9d0a624 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -10,8 +10,10 @@ struct LegacyTabView: AnyTabView { @ViewBuilder var body: some View { TabView(selection: $props.selectedPage) { - ForEach(props.children.indices, id: \.self) { index in - renderTabItem(at: index) + ForEach(props.children) { child in + if let index = props.children.firstIndex(of: child) { + renderTabItem(at: index) + } } .measureView { size in onLayout(size) @@ -27,7 +29,7 @@ struct LegacyTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] - let child = props.children[safe: index] ?? PlatformView() + let child = props.children[safe: index]?.view ?? PlatformView() let context = TabAppearContext( index: index, tabData: tabData, diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index f70cf71..229445f 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -11,15 +11,14 @@ struct NewTabView: AnyTabView { @ViewBuilder var body: some View { TabView(selection: $props.selectedPage) { - ForEach(props.children.indices, id: \.self) { index in - if let tabData = props.items[safe: index] { + ForEach(props.children) { child in + if let index = props.children.firstIndex(of: child), + let tabData = props.items[safe: index] { let isFocused = props.selectedPage == tabData.key if !tabData.hidden || isFocused { let icon = props.icons[index] - let platformChild = props.children[safe: index] ?? PlatformView() - let child = RepresentableView(view: platformChild) let context = TabAppearContext( index: index, tabData: tabData, @@ -29,7 +28,7 @@ struct NewTabView: AnyTabView { ) Tab(value: tabData.key, role: tabData.role?.convert()) { - child + RepresentableView(view: child.view) .ignoresSafeArea(.container, edges: .all) .tabAppear(using: context) } label: { diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 60bfd40..dccb41b 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -120,7 +120,6 @@ struct TabViewImpl: View { } #endif - #if !os(macOS) private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { tabBar.barTintColor = props.barTintColor diff --git a/packages/react-native-bottom-tabs/ios/TabViewProps.swift b/packages/react-native-bottom-tabs/ios/TabViewProps.swift index 28d1063..5b65f0b 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProps.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProps.swift @@ -25,7 +25,7 @@ internal enum MinimizeBehavior: String { public enum TabBarRole: String { case search - + @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) func convert() -> TabRole { switch self { @@ -35,11 +35,20 @@ public enum TabBarRole: String { } } +struct IdentifiablePlatformView: Identifiable, Equatable { + let id = UUID() + let view: PlatformView + + init(_ view: PlatformView) { + self.view = view + } +} + /** Props that component accepts. SwiftUI view gets re-rendered when ObservableObject changes. */ class TabViewProps: ObservableObject { - @Published var children: [PlatformView] = [] + @Published var children: [IdentifiablePlatformView] = [] @Published var items: [TabInfo] = [] @Published var selectedPage: String? @Published var icons: [Int: PlatformImage] = [:] diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index 17e6f9a..6a3e4ea 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -177,7 +177,7 @@ public final class TabInfo: NSObject { } override public func didUpdateReactSubviews() { - props.children = reactSubviews() + props.children = reactSubviews().map(IdentifiablePlatformView.init) } #if os(macOS) @@ -218,15 +218,15 @@ public final class TabInfo: NSObject { #endif } } - + @objc(insertChild:atIndex:) public func insertChild(_ child: UIView, at index: Int) { guard index >= 0 && index <= props.children.count else { return } - props.children.insert(child, at: index) + props.children.insert(IdentifiablePlatformView(child), at: index) } - + @objc(removeChildAtIndex:) public func removeChild(at index: Int) { guard index >= 0 && index < props.children.count else {