From 4e6764d37c09f145cb78c0d4ac8118734976fc7f Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:15:39 +0200 Subject: [PATCH 01/33] feat: create LegacyTabView implementation --- .../ios/TabView/LegacyTabView.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift new file mode 100644 index 00000000..0f423315 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -0,0 +1,64 @@ +import SwiftUI + +struct LegacyTabView: View { + @ObservedObject var props: TabViewProps + +#if os(macOS) + var tabBar: NSTabView? +#else + var tabBar: UITabBar? +#endif + + var onLayout: (_ size: CGSize) -> Void + var onSelect: (_ key: String) -> Void + var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + + @ViewBuilder + var body: some View { + TabView(selection: $props.selectedPage) { + ForEach(props.children.indices, id: \.self) { index in + renderTabItem(at: index) + } + .measureView { size in + onLayout(size) + } + } + .hideTabBar(props.tabBarHidden) + } + + @ViewBuilder + private func renderTabItem(at index: Int) -> some View { + if let tabData = props.items[safe: index] { + let isFocused = props.selectedPage == tabData.key + + if !tabData.hidden || isFocused { + let icon = props.icons[index] + let child = props.children[safe: index] ?? PlatformView() + + RepresentableView(view: child) + .ignoresSafeArea(.container, edges: .all) + .tabItem { + TabItem( + title: tabData.title, + icon: icon, + sfSymbol: tabData.sfSymbol, + labeled: props.labeled + ) + .accessibilityIdentifier(tabData.testID ?? "") + .tag(tabData.key) + .tabBadge(tabData.badge) + .onAppear { +#if !os(macOS) + updateTabBarAppearance(props, tabBar) +#endif +#if os(iOS) + if index >= 4, !isFocused { + onSelect(tabData.key) + } +#endif + } + } + } + } + } +} From 5de67097f0da257e90ec504632b90d1e697f27cc Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:16:08 +0200 Subject: [PATCH 02/33] feat: create NewTabView implementation --- .../ios/TabView/NewTabView.swift | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift new file mode 100644 index 00000000..d89a5f97 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -0,0 +1,69 @@ +import SwiftUI + +@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) +struct NewTabView: View { + @ObservedObject var props: TabViewProps + @AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization + + #if os(macOS) + var tabBar: NSTabView? + #else + var tabBar: UITabBar? + #endif + + var onLayout: (_ size: CGSize) -> Void + var onSelect: (_ key: String) -> Void + var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + + @ViewBuilder + var body: some View { + TabView(selection: $props.selectedPage) { + ForEach(props.children.indices, id: \.self) { index in + if let tabData = props.items[safe: index] { + let isFocused = props.selectedPage == tabData.key + + if !tabData.hidden || isFocused { + let icon = props.icons[index] + let role: TabRole? = nil + + let platformChild = props.children[safe: index] ?? PlatformView() + let child = RepresentableView(view: platformChild) + + Tab(value: tabData.key, role: role) { + child + .ignoresSafeArea(.container, edges: .all) + .onAppear { + #if !os(macOS) + updateTabBarAppearance(props, tabBar) + #endif + #if os(iOS) + if index >= 4 { + if props.selectedPage != tabData.key { + onSelect(tabData.key) + } + } + #endif + } + } label: { + TabItem( + title: tabData.title, + icon: icon, + sfSymbol: tabData.sfSymbol, + labeled: props.labeled + ) + } + //.badge(tabData.badge) + .customizationID(tabData.key) + .customizationBehavior(.disabled, for: .sidebar, .tabBar) + .accessibilityIdentifier(tabData.testID ?? "") + } + } + } + } + .measureView { size in + onLayout(size) + } + .tabViewCustomization($tabViewCustomization) + .hideTabBar(props.tabBarHidden) + } +} From 2a15cdeb4e02a2d568306432da1c2267b89d63ce Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:17:27 +0200 Subject: [PATCH 03/33] refactor: use different TabView depending on os version --- .../ios/TabViewImpl.swift | 65 +++++-------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index f87934f3..4f512145 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -13,21 +13,27 @@ struct TabViewImpl: View { #else @Weak var tabBar: UITabBar? #endif - + + @ViewBuilder + var tabContent: some View { + if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { + NewTabView( + props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, + updateTabBarAppearance: updateTabBarAppearance) + } else { + LegacyTabView( + props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, + updateTabBarAppearance: updateTabBarAppearance) + } + } + var onSelect: (_ key: String) -> Void var onLongPress: (_ key: String) -> Void var onLayout: (_ size: CGSize) -> Void var onTabBarMeasured: (_ height: Int) -> Void - + var body: some View { - TabView(selection: $props.selectedPage) { - ForEach(props.children.indices, id: \.self) { index in - renderTabItem(at: index) - } - .measureView { size in - onLayout(size) - } - } + tabContent #if !os(tvOS) && !os(macOS) && !os(visionOS) .onTabItemEvent { index, isLongPress in let item = props.filteredItems[safe: index] @@ -74,45 +80,6 @@ struct TabViewImpl: View { } } - @ViewBuilder - private func renderTabItem(at index: Int) -> some View { - let tabData = props.items[safe: index] - let isHidden = tabData?.hidden ?? false - let isFocused = props.selectedPage == tabData?.key - - if !isHidden || isFocused { - let child = props.children[safe: index] ?? PlatformView() - let icon = props.icons[index] - - RepresentableView(view: child) - .ignoresSafeArea(.container, edges: .all) - .tabItem { - TabItem( - title: tabData?.title, - icon: icon, - sfSymbol: tabData?.sfSymbol, - labeled: props.labeled - ) - .accessibilityIdentifier(tabData?.testID ?? "") - } - .tag(tabData?.key) - .tabBadge(tabData?.badge) - .hideTabBar(props.tabBarHidden) - .onAppear { -#if !os(macOS) - updateTabBarAppearance(props: props, tabBar: tabBar) -#endif - -#if os(iOS) - guard index >= 4, - let key = tabData?.key, - props.selectedPage != key else { return } - onSelect(key) -#endif - } - } - } - func emitHapticFeedback(longPress: Bool = false) { #if os(iOS) if !props.hapticFeedbackEnabled { From 592141e22ce1a57b65d922dfe5587dc3a1e85184 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:46:26 +0200 Subject: [PATCH 04/33] feat: Create AnyTabView protocol --- .../react-native-bottom-tabs/ios/TabView/AnyTabView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift diff --git a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift new file mode 100644 index 00000000..5be3aa93 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift @@ -0,0 +1,8 @@ +import SwiftUI + +public protocol AnyTabView: View { + var props: TabViewProps { get } + var onLayout: (_ size: CGSize) -> Void { get } + var onSelect: (_ key: String) -> Void { get } + var updateTabBarAppearance: () -> Void { get } +} From ed6f9e013ff625fe167a4c1154e1a552c2bec393 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:46:54 +0200 Subject: [PATCH 05/33] feat: Implement AnyTabView protocol in TabViews --- .../ios/TabView/LegacyTabView.swift | 16 +++++----------- .../ios/TabView/NewTabView.swift | 18 ++++++------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 0f423315..e905ea5c 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -1,17 +1,11 @@ import SwiftUI -struct LegacyTabView: View { +struct LegacyTabView: AnyTabView { @ObservedObject var props: TabViewProps - -#if os(macOS) - var tabBar: NSTabView? -#else - var tabBar: UITabBar? -#endif - var onLayout: (_ size: CGSize) -> Void - var onSelect: (_ key: String) -> Void - var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + var onLayout: (CGSize) -> Void + var onSelect: (String) -> Void + var updateTabBarAppearance: () -> Void @ViewBuilder var body: some View { @@ -49,7 +43,7 @@ struct LegacyTabView: View { .tabBadge(tabData.badge) .onAppear { #if !os(macOS) - updateTabBarAppearance(props, tabBar) + updateTabBarAppearance() #endif #if os(iOS) if index >= 4, !isFocused { diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index d89a5f97..ef109364 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -1,19 +1,13 @@ import SwiftUI @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) -struct NewTabView: View { +struct NewTabView: AnyTabView { @ObservedObject var props: TabViewProps @AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization - - #if os(macOS) - var tabBar: NSTabView? - #else - var tabBar: UITabBar? - #endif - - var onLayout: (_ size: CGSize) -> Void - var onSelect: (_ key: String) -> Void - var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + + var onLayout: (CGSize) -> Void + var onSelect: (String) -> Void + var updateTabBarAppearance: () -> Void @ViewBuilder var body: some View { @@ -34,7 +28,7 @@ struct NewTabView: View { .ignoresSafeArea(.container, edges: .all) .onAppear { #if !os(macOS) - updateTabBarAppearance(props, tabBar) + updateTabBarAppearance() #endif #if os(iOS) if index >= 4 { From c1c1205af3aad3ebb8424462f212c8ab32d2f0da Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:47:22 +0200 Subject: [PATCH 06/33] refactor: Use AnyTabView as tabContent --- .../ios/TabViewImpl.swift | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 4f512145..0d7933ee 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -15,15 +15,25 @@ struct TabViewImpl: View { #endif @ViewBuilder - var tabContent: some View { + var tabContent: some AnyTabView { if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { NewTabView( - props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, - updateTabBarAppearance: updateTabBarAppearance) + props: props, + onLayout: onLayout, + onSelect: onSelect, + updateTabBarAppearance: { + updateTabBarAppearance(props: props, tabBar: tabBar) + } + ) } else { LegacyTabView( - props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, - updateTabBarAppearance: updateTabBarAppearance) + props: props, + onLayout: onLayout, + onSelect: onSelect, + updateTabBarAppearance: { + updateTabBarAppearance(props: props, tabBar: tabBar) + } + ) } } From 5ec3e2f833130a801e37626602ebc3650bb61af0 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:47:33 +0200 Subject: [PATCH 07/33] feat: Create TabAppearModifier.swift --- .../ios/TabAppearModifier.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabAppearModifier.swift diff --git a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift new file mode 100644 index 00000000..819b8dfa --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct TabAppearContext { + let index: Int + let tabData: TabInfo + let props: TabViewProps + let updateTabBarAppearance: () -> Void + let onSelect: (_ key: String) -> Void +} + +struct TabAppearModifier: ViewModifier { + let context: TabAppearContext + + func body(content: Content) -> some View { + content.onAppear { +#if !os(macOS) + context.updateTabBarAppearance() +#endif + +#if os(iOS) + if context.index >= 4, context.props.selectedPage != context.tabData.key { + context.onSelect(context.tabData.key) + } +#endif + } + } +} + +extension View { + func tabAppear(using context: TabAppearContext) -> some View { + self.modifier(TabAppearModifier(context: context)) + } +} From dfec0761dbdc155c9c1a8449da6092755b559372 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:48:08 +0200 Subject: [PATCH 08/33] refactor: Use tabAppear modifier --- .../ios/TabView/LegacyTabView.swift | 18 ++++++++--------- .../ios/TabView/NewTabView.swift | 20 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index e905ea5c..b2a57176 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -28,6 +28,13 @@ struct LegacyTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] let child = props.children[safe: index] ?? PlatformView() + let context = TabAppearContext( + index: index, + tabData: tabData, + props: props, + updateTabBarAppearance: updateTabBarAppearance, + onSelect: onSelect + ) RepresentableView(view: child) .ignoresSafeArea(.container, edges: .all) @@ -41,16 +48,7 @@ struct LegacyTabView: AnyTabView { .accessibilityIdentifier(tabData.testID ?? "") .tag(tabData.key) .tabBadge(tabData.badge) - .onAppear { -#if !os(macOS) - updateTabBarAppearance() -#endif -#if os(iOS) - if index >= 4, !isFocused { - onSelect(tabData.key) - } -#endif - } + .tabAppear(using: context) } } } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index ef109364..192bcbf0 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -22,22 +22,18 @@ struct NewTabView: AnyTabView { let platformChild = props.children[safe: index] ?? PlatformView() let child = RepresentableView(view: platformChild) + let context = TabAppearContext( + index: index, + tabData: tabData, + props: props, + updateTabBarAppearance: updateTabBarAppearance, + onSelect: onSelect + ) Tab(value: tabData.key, role: role) { child .ignoresSafeArea(.container, edges: .all) - .onAppear { - #if !os(macOS) - updateTabBarAppearance() - #endif - #if os(iOS) - if index >= 4 { - if props.selectedPage != tabData.key { - onSelect(tabData.key) - } - } - #endif - } + .tabAppear(using: context) } label: { TabItem( title: tabData.title, From 2f444ac9c1c0133314df3712b82b650bbd68b1fc Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:50:36 +0200 Subject: [PATCH 09/33] fix: move LegacyTabView measureView --- .../ios/TabView/LegacyTabView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index b2a57176..51ddd8b7 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -13,9 +13,9 @@ struct LegacyTabView: AnyTabView { ForEach(props.children.indices, id: \.self) { index in renderTabItem(at: index) } - .measureView { size in - onLayout(size) - } + } + .measureView { size in + onLayout(size) } .hideTabBar(props.tabBarHidden) } From 69837bdc41f01dd64dcf6d893ab70bd50b70c9bb Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:52:03 +0200 Subject: [PATCH 10/33] chore: remove tabViewCustomization --- .../react-native-bottom-tabs/ios/TabView/NewTabView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 192bcbf0..3f456be0 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -3,7 +3,6 @@ import SwiftUI @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) struct NewTabView: AnyTabView { @ObservedObject var props: TabViewProps - @AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization var onLayout: (CGSize) -> Void var onSelect: (String) -> Void @@ -43,8 +42,6 @@ struct NewTabView: AnyTabView { ) } //.badge(tabData.badge) - .customizationID(tabData.key) - .customizationBehavior(.disabled, for: .sidebar, .tabBar) .accessibilityIdentifier(tabData.testID ?? "") } } @@ -53,7 +50,6 @@ struct NewTabView: AnyTabView { .measureView { size in onLayout(size) } - .tabViewCustomization($tabViewCustomization) .hideTabBar(props.tabBarHidden) } } From f0dfc85591ab8cabac3a1f390a0ff46ca0c7344f Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:56:36 +0200 Subject: [PATCH 11/33] fix: remove props from AnyTabView as AnyTabView is internal --- packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift index 5be3aa93..168565a5 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift @@ -1,7 +1,6 @@ import SwiftUI public protocol AnyTabView: View { - var props: TabViewProps { get } var onLayout: (_ size: CGSize) -> Void { get } var onSelect: (_ key: String) -> Void { get } var updateTabBarAppearance: () -> Void { get } From c610cdacefe84c4456c7000931a611547075c47d Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:01:16 +0200 Subject: [PATCH 12/33] fix: revert tabContent to some View --- packages/react-native-bottom-tabs/ios/TabViewImpl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 0d7933ee..5c0ac864 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -15,7 +15,7 @@ struct TabViewImpl: View { #endif @ViewBuilder - var tabContent: some AnyTabView { + var tabContent: some View { if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { NewTabView( props: props, From 8fd1f7bed176fa05562dadec802861b25dae9e4f Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:06:41 +0200 Subject: [PATCH 13/33] fix: revert move LegacyTabView measureView --- .../ios/TabView/LegacyTabView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 51ddd8b7..b2a57176 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -13,9 +13,9 @@ struct LegacyTabView: AnyTabView { ForEach(props.children.indices, id: \.self) { index in renderTabItem(at: index) } - } - .measureView { size in - onLayout(size) + .measureView { size in + onLayout(size) + } } .hideTabBar(props.tabBarHidden) } From dc4ac1b98ad7951824fc8e6990d5acc4c248b67f Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:15:39 +0200 Subject: [PATCH 14/33] feat: create LegacyTabView implementation --- .../ios/TabView/LegacyTabView.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift new file mode 100644 index 00000000..0f423315 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -0,0 +1,64 @@ +import SwiftUI + +struct LegacyTabView: View { + @ObservedObject var props: TabViewProps + +#if os(macOS) + var tabBar: NSTabView? +#else + var tabBar: UITabBar? +#endif + + var onLayout: (_ size: CGSize) -> Void + var onSelect: (_ key: String) -> Void + var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + + @ViewBuilder + var body: some View { + TabView(selection: $props.selectedPage) { + ForEach(props.children.indices, id: \.self) { index in + renderTabItem(at: index) + } + .measureView { size in + onLayout(size) + } + } + .hideTabBar(props.tabBarHidden) + } + + @ViewBuilder + private func renderTabItem(at index: Int) -> some View { + if let tabData = props.items[safe: index] { + let isFocused = props.selectedPage == tabData.key + + if !tabData.hidden || isFocused { + let icon = props.icons[index] + let child = props.children[safe: index] ?? PlatformView() + + RepresentableView(view: child) + .ignoresSafeArea(.container, edges: .all) + .tabItem { + TabItem( + title: tabData.title, + icon: icon, + sfSymbol: tabData.sfSymbol, + labeled: props.labeled + ) + .accessibilityIdentifier(tabData.testID ?? "") + .tag(tabData.key) + .tabBadge(tabData.badge) + .onAppear { +#if !os(macOS) + updateTabBarAppearance(props, tabBar) +#endif +#if os(iOS) + if index >= 4, !isFocused { + onSelect(tabData.key) + } +#endif + } + } + } + } + } +} From d5f0dace7302e0874f90ad7662cdef4bd28cd4c4 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:16:08 +0200 Subject: [PATCH 15/33] feat: create NewTabView implementation --- .../ios/TabView/NewTabView.swift | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift new file mode 100644 index 00000000..d89a5f97 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -0,0 +1,69 @@ +import SwiftUI + +@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) +struct NewTabView: View { + @ObservedObject var props: TabViewProps + @AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization + + #if os(macOS) + var tabBar: NSTabView? + #else + var tabBar: UITabBar? + #endif + + var onLayout: (_ size: CGSize) -> Void + var onSelect: (_ key: String) -> Void + var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + + @ViewBuilder + var body: some View { + TabView(selection: $props.selectedPage) { + ForEach(props.children.indices, id: \.self) { index in + if let tabData = props.items[safe: index] { + let isFocused = props.selectedPage == tabData.key + + if !tabData.hidden || isFocused { + let icon = props.icons[index] + let role: TabRole? = nil + + let platformChild = props.children[safe: index] ?? PlatformView() + let child = RepresentableView(view: platformChild) + + Tab(value: tabData.key, role: role) { + child + .ignoresSafeArea(.container, edges: .all) + .onAppear { + #if !os(macOS) + updateTabBarAppearance(props, tabBar) + #endif + #if os(iOS) + if index >= 4 { + if props.selectedPage != tabData.key { + onSelect(tabData.key) + } + } + #endif + } + } label: { + TabItem( + title: tabData.title, + icon: icon, + sfSymbol: tabData.sfSymbol, + labeled: props.labeled + ) + } + //.badge(tabData.badge) + .customizationID(tabData.key) + .customizationBehavior(.disabled, for: .sidebar, .tabBar) + .accessibilityIdentifier(tabData.testID ?? "") + } + } + } + } + .measureView { size in + onLayout(size) + } + .tabViewCustomization($tabViewCustomization) + .hideTabBar(props.tabBarHidden) + } +} From 23cb5f37bdbfa0917676373bb744c75eeaae713f Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:17:27 +0200 Subject: [PATCH 16/33] refactor: use different TabView depending on os version --- .../ios/TabViewImpl.swift | 641 +++++++++--------- 1 file changed, 303 insertions(+), 338 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 92b9539a..448be84a 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -3,381 +3,346 @@ import React import SwiftUI @_spi(Advanced) import SwiftUIIntrospect -/** - SwiftUI implementation of TabView used to render React Native views. - */ +/// SwiftUI implementation of TabView used to render React Native views. struct TabViewImpl: View { - @ObservedObject var props: TabViewProps -#if os(macOS) - @Weak var tabBar: NSTabView? -#else - @Weak var tabBar: UITabBar? -#endif - - var onSelect: (_ key: String) -> Void - var onLongPress: (_ key: String) -> Void - var onLayout: (_ size: CGSize) -> Void - var onTabBarMeasured: (_ height: Int) -> Void - - var body: some View { - TabView(selection: $props.selectedPage) { - ForEach(props.children.indices, id: \.self) { index in - renderTabItem(at: index) - } - .measureView { size in - onLayout(size) - } - } - .tabBarMinimizeBehavior(props.minimizeBehavior) -#if !os(tvOS) && !os(macOS) && !os(visionOS) - .onTabItemEvent { index, isLongPress in - let item = props.filteredItems[safe: index] - guard let key = item?.key else { return } - - if isLongPress { - onLongPress(key) - emitHapticFeedback(longPress: true) - } else { - onSelect(key) - emitHapticFeedback() - } - } -#endif - .introspectTabView { tabController in -#if os(macOS) - tabBar = tabController -#else - tabBar = tabController.tabBar - if !props.tabBarHidden { - onTabBarMeasured( - Int(tabController.tabBar.frame.size.height) - ) - } -#endif - } -#if !os(macOS) - .configureAppearance(props: props, tabBar: tabBar) -#endif - .tintColor(props.selectedActiveTintColor) - .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) - .onChange(of: props.selectedPage ?? "") { newValue in -#if !os(macOS) - if props.disablePageAnimations { - UIView.setAnimationsEnabled(false) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UIView.setAnimationsEnabled(true) - } - } -#endif -#if os(tvOS) || os(macOS) || os(visionOS) - onSelect(newValue) -#endif - } - } - - @ViewBuilder - private func renderTabItem(at index: Int) -> some View { - let tabData = props.items[safe: index] - let isHidden = tabData?.hidden ?? false - let isFocused = props.selectedPage == tabData?.key - - if !isHidden || isFocused { - let child = props.children[safe: index] ?? PlatformView() - let icon = props.icons[index] - - RepresentableView(view: child) - .ignoresSafeArea(.container, edges: .all) - .tabItem { - TabItem( - title: tabData?.title, - icon: icon, - sfSymbol: tabData?.sfSymbol, - labeled: props.labeled - ) - .accessibilityIdentifier(tabData?.testID ?? "") - } - .tag(tabData?.key) - .tabBadge(tabData?.badge) - .hideTabBar(props.tabBarHidden) - .onAppear { -#if !os(macOS) - updateTabBarAppearance(props: props, tabBar: tabBar) -#endif - -#if os(iOS) - guard index >= 4, - let key = tabData?.key, - props.selectedPage != key else { return } - onSelect(key) -#endif + @ObservedObject var props: TabViewProps + #if os(macOS) + @Weak var tabBar: NSTabView? + #else + @Weak var tabBar: UITabBar? + #endif + + @ViewBuilder + var tabContent: some View { + if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { + NewTabView( + props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, + updateTabBarAppearance: updateTabBarAppearance) + } else { + LegacyTabView( + props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, + updateTabBarAppearance: updateTabBarAppearance) } } - } - func emitHapticFeedback(longPress: Bool = false) { -#if os(iOS) - if !props.hapticFeedbackEnabled { - return + var onSelect: (_ key: String) -> Void + var onLongPress: (_ key: String) -> Void + var onLayout: (_ size: CGSize) -> Void + var onTabBarMeasured: (_ height: Int) -> Void + + var body: some View { + tabContent + .tabBarMinimizeBehavior(props.minimizeBehavior) + #if !os(tvOS) && !os(macOS) && !os(visionOS) + .onTabItemEvent { index, isLongPress in + let item = props.filteredItems[safe: index] + guard let key = item?.key else { return } + + if isLongPress { + onLongPress(key) + emitHapticFeedback(longPress: true) + } else { + onSelect(key) + emitHapticFeedback() + } + } + #endif + .introspectTabView { tabController in + #if os(macOS) + tabBar = tabController + #else + tabBar = tabController.tabBar + if !props.tabBarHidden { + onTabBarMeasured( + Int(tabController.tabBar.frame.size.height) + ) + } + #endif + } + #if !os(macOS) + .configureAppearance(props: props, tabBar: tabBar) + #endif + .tintColor(props.selectedActiveTintColor) + .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) + .onChange(of: props.selectedPage ?? "") { newValue in + #if !os(macOS) + if props.disablePageAnimations { + UIView.setAnimationsEnabled(false) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + UIView.setAnimationsEnabled(true) + } + } + #endif + #if os(tvOS) || os(macOS) || os(visionOS) + onSelect(newValue) + #endif + } } - if longPress { - UINotificationFeedbackGenerator().notificationOccurred(.success) - } else { - UISelectionFeedbackGenerator().selectionChanged() + func emitHapticFeedback(longPress: Bool = false) { + #if os(iOS) + if !props.hapticFeedbackEnabled { + return + } + + if longPress { + UINotificationFeedbackGenerator().notificationOccurred(.success) + } else { + UISelectionFeedbackGenerator().selectionChanged() + } + #endif } -#endif - } } #if !os(macOS) -private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { - guard let tabBar else { return } + private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { + guard let tabBar else { return } - tabBar.isHidden = props.tabBarHidden + tabBar.isHidden = props.tabBarHidden - if props.scrollEdgeAppearance == "transparent" { - configureTransparentAppearance(tabBar: tabBar, props: props) - return - } + if props.scrollEdgeAppearance == "transparent" { + configureTransparentAppearance(tabBar: tabBar, props: props) + return + } - configureStandardAppearance(tabBar: tabBar, props: props) -} + configureStandardAppearance(tabBar: tabBar, props: props) + } #endif private func createFontAttributes( - size: CGFloat, - family: String?, - weight: String?, - inactiveTintColor: PlatformColor? + size: CGFloat, + family: String?, + weight: String?, + inactiveTintColor: PlatformColor? ) -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] = [:] - - if family != nil || weight != nil { - attributes[.font] = RCTFont.update( - nil, - withFamily: family, - size: NSNumber(value: size), - weight: weight, - style: nil, - variant: nil, - scaleMultiplier: 1.0 - ) - } else { - attributes[.font] = UIFont.boldSystemFont(ofSize: size) - } - - return attributes + var attributes: [NSAttributedString.Key: Any] = [:] + + if family != nil || weight != nil { + attributes[.font] = RCTFont.update( + nil, + withFamily: family, + size: NSNumber(value: size), + weight: weight, + style: nil, + variant: nil, + scaleMultiplier: 1.0 + ) + } else { + attributes[.font] = UIFont.boldSystemFont(ofSize: size) + } + + return attributes } #if os(tvOS) -let tabBarDefaultFontSize: CGFloat = 30.0 + let tabBarDefaultFontSize: CGFloat = 30.0 #else -let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize + let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize #endif #if !os(macOS) -private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { - tabBar.barTintColor = props.barTintColor -#if !os(visionOS) - tabBar.isTranslucent = props.translucent -#endif - tabBar.unselectedItemTintColor = props.inactiveTintColor + private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { + tabBar.barTintColor = props.barTintColor + #if !os(visionOS) + tabBar.isTranslucent = props.translucent + #endif + tabBar.unselectedItemTintColor = props.inactiveTintColor + + guard let items = tabBar.items else { return } + + let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize + let attributes = createFontAttributes( + size: fontSize, + family: props.fontFamily, + weight: props.fontWeight, + inactiveTintColor: nil + ) - guard let items = tabBar.items else { return } + items.forEach { item in + item.setTitleTextAttributes(attributes, for: .normal) + } + } - let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize - let attributes = createFontAttributes( - size: fontSize, - family: props.fontFamily, - weight: props.fontWeight, - inactiveTintColor: nil - ) + private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { + let appearance = UITabBarAppearance() - items.forEach { item in - item.setTitleTextAttributes(attributes, for: .normal) - } -} + // Configure background + switch props.scrollEdgeAppearance { + case "opaque": + appearance.configureWithOpaqueBackground() + default: + appearance.configureWithDefaultBackground() + } -private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { - let appearance = UITabBarAppearance() - - // Configure background - switch props.scrollEdgeAppearance { - case "opaque": - appearance.configureWithOpaqueBackground() - default: - appearance.configureWithDefaultBackground() - } - - if props.translucent == false { - appearance.configureWithOpaqueBackground() - } - - if props.barTintColor != nil { - appearance.backgroundColor = props.barTintColor - } - - // Configure item appearance - let itemAppearance = UITabBarItemAppearance() - let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize - - var attributes = createFontAttributes( - size: fontSize, - family: props.fontFamily, - weight: props.fontWeight, - inactiveTintColor: props.inactiveTintColor - ) - - if let inactiveTintColor = props.inactiveTintColor { - attributes[.foregroundColor] = inactiveTintColor - } - - if let inactiveTintColor = props.inactiveTintColor { - itemAppearance.normal.iconColor = inactiveTintColor - } - - itemAppearance.normal.titleTextAttributes = attributes - - // Apply item appearance to all layouts - appearance.stackedLayoutAppearance = itemAppearance - appearance.inlineLayoutAppearance = itemAppearance - appearance.compactInlineLayoutAppearance = itemAppearance - - // Apply final appearance - tabBar.standardAppearance = appearance - if #available(iOS 15.0, *) { - tabBar.scrollEdgeAppearance = appearance.copy() - } -} -#endif + if props.translucent == false { + appearance.configureWithOpaqueBackground() + } -extension View { - @ViewBuilder - func getSidebarAdaptable(enabled: Bool) -> some View { - if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) { - if enabled { -#if compiler(>=6.0) - self.tabViewStyle(.sidebarAdaptable) -#else - self -#endif - } else { - self - } - } else { - self + if props.barTintColor != nil { + appearance.backgroundColor = props.barTintColor + } + + // Configure item appearance + let itemAppearance = UITabBarItemAppearance() + let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize + + var attributes = createFontAttributes( + size: fontSize, + family: props.fontFamily, + weight: props.fontWeight, + inactiveTintColor: props.inactiveTintColor + ) + + if let inactiveTintColor = props.inactiveTintColor { + attributes[.foregroundColor] = inactiveTintColor + } + + if let inactiveTintColor = props.inactiveTintColor { + itemAppearance.normal.iconColor = inactiveTintColor + } + + itemAppearance.normal.titleTextAttributes = attributes + + // Apply item appearance to all layouts + appearance.stackedLayoutAppearance = itemAppearance + appearance.inlineLayoutAppearance = itemAppearance + appearance.compactInlineLayoutAppearance = itemAppearance + + // Apply final appearance + tabBar.standardAppearance = appearance + if #available(iOS 15.0, *) { + tabBar.scrollEdgeAppearance = appearance.copy() + } } - } - - @ViewBuilder - func tabBadge(_ data: String?) -> some View { - if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) { - if let data { -#if !os(tvOS) - self.badge(data) -#else - self #endif - } else { - self - } - } else { - self + +extension View { + @ViewBuilder + func getSidebarAdaptable(enabled: Bool) -> some View { + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) { + if enabled { + #if compiler(>=6.0) + self.tabViewStyle(.sidebarAdaptable) + #else + self + #endif + } else { + self + } + } else { + self + } } - } -#if !os(macOS) - @ViewBuilder - func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View { - self - .onChange(of: props.barTintColor) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.scrollEdgeAppearance) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.translucent) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.inactiveTintColor) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.selectedActiveTintColor) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.fontSize) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.fontFamily) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.fontWeight) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.tabBarHidden) { newValue in - tabBar?.isHidden = newValue - } - } -#endif + @ViewBuilder + func tabBadge(_ data: String?) -> some View { + if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) { + if let data { + #if !os(tvOS) + self.badge(data) + #else + self + #endif + } else { + self + } + } else { + self + } + } - @ViewBuilder - func tintColor(_ color: PlatformColor?) -> some View { - if let color { - let color = Color(color) - if #available(iOS 16.0, tvOS 16.0, macOS 13.0, *) { - self.tint(color) - } else { - self.accentColor(color) - } - } else { - self + #if !os(macOS) + @ViewBuilder + func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View { + self + .onChange(of: props.barTintColor) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.scrollEdgeAppearance) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.translucent) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.inactiveTintColor) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.selectedActiveTintColor) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontSize) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontFamily) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontWeight) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.tabBarHidden) { newValue in + tabBar?.isHidden = newValue + } + } + #endif + + @ViewBuilder + func tintColor(_ color: PlatformColor?) -> some View { + if let color { + let color = Color(color) + if #available(iOS 16.0, tvOS 16.0, macOS 13.0, *) { + self.tint(color) + } else { + self.accentColor(color) + } + } else { + self + } } - } - - @ViewBuilder - func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View { -#if compiler(>=6.2) - if #available(iOS 26.0, *) { - if let behavior { - self.tabBarMinimizeBehavior(behavior.convert()) - } else { - self - } - } else { - self + + @ViewBuilder + func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View { + #if compiler(>=6.2) + if #available(iOS 26.0, *) { + if let behavior { + self.tabBarMinimizeBehavior(behavior.convert()) + } else { + self + } + } else { + self + } + #else + self + #endif } -#else - self -#endif - } - @ViewBuilder - func hideTabBar(_ flag: Bool) -> some View { -#if !os(macOS) - if flag { - if #available(iOS 16.0, tvOS 16.0, *) { - self.toolbar(.hidden, for: .tabBar) - } else { - // We fallback to isHidden on UITabBar - self - } - } else { - self + @ViewBuilder + func hideTabBar(_ flag: Bool) -> some View { + #if !os(macOS) + if flag { + if #available(iOS 16.0, tvOS 16.0, *) { + self.toolbar(.hidden, for: .tabBar) + } else { + // We fallback to isHidden on UITabBar + self + } + } else { + self + } + #else + self + #endif } -#else - self -#endif - } - - // Allows TabView to use unfilled SFSymbols. - // By default they are always filled. - @ViewBuilder - func noneSymbolVariant() -> some View { - if #available(iOS 15.0, tvOS 15.0, macOS 13.0, *) { - self - .environment(\.symbolVariants, .none) - } else { - self + + // Allows TabView to use unfilled SFSymbols. + // By default they are always filled. + @ViewBuilder + func noneSymbolVariant() -> some View { + if #available(iOS 15.0, tvOS 15.0, macOS 13.0, *) { + self + .environment(\.symbolVariants, .none) + } else { + self + } } - } } From da02dba9bdc3ab6cd23f0ebf602c0c3517bee056 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:46:26 +0200 Subject: [PATCH 17/33] feat: Create AnyTabView protocol --- .../react-native-bottom-tabs/ios/TabView/AnyTabView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift diff --git a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift new file mode 100644 index 00000000..5be3aa93 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift @@ -0,0 +1,8 @@ +import SwiftUI + +public protocol AnyTabView: View { + var props: TabViewProps { get } + var onLayout: (_ size: CGSize) -> Void { get } + var onSelect: (_ key: String) -> Void { get } + var updateTabBarAppearance: () -> Void { get } +} From f330ee0829ea6b8ae3e42cccaeeaeeb4b1ebb106 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:46:54 +0200 Subject: [PATCH 18/33] feat: Implement AnyTabView protocol in TabViews --- .../ios/TabView/LegacyTabView.swift | 16 +++++----------- .../ios/TabView/NewTabView.swift | 18 ++++++------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 0f423315..e905ea5c 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -1,17 +1,11 @@ import SwiftUI -struct LegacyTabView: View { +struct LegacyTabView: AnyTabView { @ObservedObject var props: TabViewProps - -#if os(macOS) - var tabBar: NSTabView? -#else - var tabBar: UITabBar? -#endif - var onLayout: (_ size: CGSize) -> Void - var onSelect: (_ key: String) -> Void - var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + var onLayout: (CGSize) -> Void + var onSelect: (String) -> Void + var updateTabBarAppearance: () -> Void @ViewBuilder var body: some View { @@ -49,7 +43,7 @@ struct LegacyTabView: View { .tabBadge(tabData.badge) .onAppear { #if !os(macOS) - updateTabBarAppearance(props, tabBar) + updateTabBarAppearance() #endif #if os(iOS) if index >= 4, !isFocused { diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index d89a5f97..ef109364 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -1,19 +1,13 @@ import SwiftUI @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) -struct NewTabView: View { +struct NewTabView: AnyTabView { @ObservedObject var props: TabViewProps @AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization - - #if os(macOS) - var tabBar: NSTabView? - #else - var tabBar: UITabBar? - #endif - - var onLayout: (_ size: CGSize) -> Void - var onSelect: (_ key: String) -> Void - var updateTabBarAppearance: (_ props: TabViewProps, _ tabBar: UITabBar?) -> Void + + var onLayout: (CGSize) -> Void + var onSelect: (String) -> Void + var updateTabBarAppearance: () -> Void @ViewBuilder var body: some View { @@ -34,7 +28,7 @@ struct NewTabView: View { .ignoresSafeArea(.container, edges: .all) .onAppear { #if !os(macOS) - updateTabBarAppearance(props, tabBar) + updateTabBarAppearance() #endif #if os(iOS) if index >= 4 { From b4a22a859f4b13f108c32c84438a8a085d9a372b Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:47:22 +0200 Subject: [PATCH 19/33] refactor: Use AnyTabView as tabContent --- .../ios/TabViewImpl.swift | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 448be84a..918dd271 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -13,15 +13,25 @@ struct TabViewImpl: View { #endif @ViewBuilder - var tabContent: some View { + var tabContent: some AnyTabView { if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { NewTabView( - props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, - updateTabBarAppearance: updateTabBarAppearance) + props: props, + onLayout: onLayout, + onSelect: onSelect, + updateTabBarAppearance: { + updateTabBarAppearance(props: props, tabBar: tabBar) + } + ) } else { LegacyTabView( - props: props, tabBar: tabBar, onLayout: onLayout, onSelect: onSelect, - updateTabBarAppearance: updateTabBarAppearance) + props: props, + onLayout: onLayout, + onSelect: onSelect, + updateTabBarAppearance: { + updateTabBarAppearance(props: props, tabBar: tabBar) + } + ) } } From 68c5aa128016a63d9bcaa33d11cc5a946a20ec29 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:47:33 +0200 Subject: [PATCH 20/33] feat: Create TabAppearModifier.swift --- .../ios/TabAppearModifier.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/react-native-bottom-tabs/ios/TabAppearModifier.swift diff --git a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift new file mode 100644 index 00000000..819b8dfa --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct TabAppearContext { + let index: Int + let tabData: TabInfo + let props: TabViewProps + let updateTabBarAppearance: () -> Void + let onSelect: (_ key: String) -> Void +} + +struct TabAppearModifier: ViewModifier { + let context: TabAppearContext + + func body(content: Content) -> some View { + content.onAppear { +#if !os(macOS) + context.updateTabBarAppearance() +#endif + +#if os(iOS) + if context.index >= 4, context.props.selectedPage != context.tabData.key { + context.onSelect(context.tabData.key) + } +#endif + } + } +} + +extension View { + func tabAppear(using context: TabAppearContext) -> some View { + self.modifier(TabAppearModifier(context: context)) + } +} From d15a863486db4b654cffe0ae626e9d5d051235ae Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:48:08 +0200 Subject: [PATCH 21/33] refactor: Use tabAppear modifier --- .../ios/TabView/LegacyTabView.swift | 18 ++++++++--------- .../ios/TabView/NewTabView.swift | 20 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index e905ea5c..b2a57176 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -28,6 +28,13 @@ struct LegacyTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] let child = props.children[safe: index] ?? PlatformView() + let context = TabAppearContext( + index: index, + tabData: tabData, + props: props, + updateTabBarAppearance: updateTabBarAppearance, + onSelect: onSelect + ) RepresentableView(view: child) .ignoresSafeArea(.container, edges: .all) @@ -41,16 +48,7 @@ struct LegacyTabView: AnyTabView { .accessibilityIdentifier(tabData.testID ?? "") .tag(tabData.key) .tabBadge(tabData.badge) - .onAppear { -#if !os(macOS) - updateTabBarAppearance() -#endif -#if os(iOS) - if index >= 4, !isFocused { - onSelect(tabData.key) - } -#endif - } + .tabAppear(using: context) } } } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index ef109364..192bcbf0 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -22,22 +22,18 @@ struct NewTabView: AnyTabView { let platformChild = props.children[safe: index] ?? PlatformView() let child = RepresentableView(view: platformChild) + let context = TabAppearContext( + index: index, + tabData: tabData, + props: props, + updateTabBarAppearance: updateTabBarAppearance, + onSelect: onSelect + ) Tab(value: tabData.key, role: role) { child .ignoresSafeArea(.container, edges: .all) - .onAppear { - #if !os(macOS) - updateTabBarAppearance() - #endif - #if os(iOS) - if index >= 4 { - if props.selectedPage != tabData.key { - onSelect(tabData.key) - } - } - #endif - } + .tabAppear(using: context) } label: { TabItem( title: tabData.title, From 8444bfc0b1e75827856e5777b225b0ea4ce9471c Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:50:36 +0200 Subject: [PATCH 22/33] fix: move LegacyTabView measureView --- .../ios/TabView/LegacyTabView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index b2a57176..51ddd8b7 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -13,9 +13,9 @@ struct LegacyTabView: AnyTabView { ForEach(props.children.indices, id: \.self) { index in renderTabItem(at: index) } - .measureView { size in - onLayout(size) - } + } + .measureView { size in + onLayout(size) } .hideTabBar(props.tabBarHidden) } From 65ada5f2dc68fe5da6e1d4e590b415962e7f353a Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:52:03 +0200 Subject: [PATCH 23/33] chore: remove tabViewCustomization --- .../react-native-bottom-tabs/ios/TabView/NewTabView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 192bcbf0..3f456be0 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -3,7 +3,6 @@ import SwiftUI @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) struct NewTabView: AnyTabView { @ObservedObject var props: TabViewProps - @AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization var onLayout: (CGSize) -> Void var onSelect: (String) -> Void @@ -43,8 +42,6 @@ struct NewTabView: AnyTabView { ) } //.badge(tabData.badge) - .customizationID(tabData.key) - .customizationBehavior(.disabled, for: .sidebar, .tabBar) .accessibilityIdentifier(tabData.testID ?? "") } } @@ -53,7 +50,6 @@ struct NewTabView: AnyTabView { .measureView { size in onLayout(size) } - .tabViewCustomization($tabViewCustomization) .hideTabBar(props.tabBarHidden) } } From 5e8b5caf6c541b296d6d1e4a53ca703ce85838bb Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:56:36 +0200 Subject: [PATCH 24/33] fix: remove props from AnyTabView as AnyTabView is internal --- packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift index 5be3aa93..168565a5 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift @@ -1,7 +1,6 @@ import SwiftUI public protocol AnyTabView: View { - var props: TabViewProps { get } var onLayout: (_ size: CGSize) -> Void { get } var onSelect: (_ key: String) -> Void { get } var updateTabBarAppearance: () -> Void { get } From 9b775718584620143d444be51a26956cb262bccb Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:01:16 +0200 Subject: [PATCH 25/33] fix: revert tabContent to some View --- .../ios/TabAppearModifier.swift | 4 ++-- .../ios/TabView/LegacyTabView.swift | 10 +++++----- .../ios/TabView/NewTabView.swift | 4 ++-- .../react-native-bottom-tabs/ios/TabViewImpl.swift | 13 +++++-------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift index 819b8dfa..922b5b5b 100644 --- a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift +++ b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift @@ -10,13 +10,13 @@ struct TabAppearContext { struct TabAppearModifier: ViewModifier { let context: TabAppearContext - + func body(content: Content) -> some View { content.onAppear { #if !os(macOS) context.updateTabBarAppearance() #endif - + #if os(iOS) if context.index >= 4, context.props.selectedPage != context.tabData.key { context.onSelect(context.tabData.key) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 51ddd8b7..9e364568 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -2,11 +2,11 @@ import SwiftUI struct LegacyTabView: AnyTabView { @ObservedObject var props: TabViewProps - + var onLayout: (CGSize) -> Void var onSelect: (String) -> Void var updateTabBarAppearance: () -> Void - + @ViewBuilder var body: some View { TabView(selection: $props.selectedPage) { @@ -19,12 +19,12 @@ struct LegacyTabView: AnyTabView { } .hideTabBar(props.tabBarHidden) } - + @ViewBuilder private func renderTabItem(at index: Int) -> some View { if let tabData = props.items[safe: index] { let isFocused = props.selectedPage == tabData.key - + if !tabData.hidden || isFocused { let icon = props.icons[index] let child = props.children[safe: index] ?? PlatformView() @@ -35,7 +35,7 @@ struct LegacyTabView: AnyTabView { updateTabBarAppearance: updateTabBarAppearance, onSelect: onSelect ) - + RepresentableView(view: child) .ignoresSafeArea(.container, edges: .all) .tabItem { diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 3f456be0..2dc0b264 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -3,7 +3,7 @@ import SwiftUI @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) struct NewTabView: AnyTabView { @ObservedObject var props: TabViewProps - + var onLayout: (CGSize) -> Void var onSelect: (String) -> Void var updateTabBarAppearance: () -> Void @@ -41,7 +41,7 @@ struct NewTabView: AnyTabView { labeled: props.labeled ) } - //.badge(tabData.badge) + // .badge(tabData.badge) .accessibilityIdentifier(tabData.testID ?? "") } } diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 918dd271..c9c9aa49 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -13,25 +13,23 @@ struct TabViewImpl: View { #endif @ViewBuilder - var tabContent: some AnyTabView { + var tabContent: some View { if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { NewTabView( props: props, onLayout: onLayout, - onSelect: onSelect, - updateTabBarAppearance: { + onSelect: onSelect + ) { updateTabBarAppearance(props: props, tabBar: tabBar) } - ) } else { LegacyTabView( props: props, onLayout: onLayout, - onSelect: onSelect, - updateTabBarAppearance: { + onSelect: onSelect + ) { updateTabBarAppearance(props: props, tabBar: tabBar) } - ) } } @@ -42,7 +40,6 @@ struct TabViewImpl: View { var body: some View { tabContent - .tabBarMinimizeBehavior(props.minimizeBehavior) #if !os(tvOS) && !os(macOS) && !os(visionOS) .onTabItemEvent { index, isLongPress in let item = props.filteredItems[safe: index] From d2d4b34d8f6f4b65dbaadc697c818580bdd8ff98 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:06:41 +0200 Subject: [PATCH 26/33] fix: revert move LegacyTabView measureView --- .../ios/TabView/LegacyTabView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 9e364568..b782f807 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -13,9 +13,9 @@ struct LegacyTabView: AnyTabView { ForEach(props.children.indices, id: \.self) { index in renderTabItem(at: index) } - } - .measureView { size in - onLayout(size) + .measureView { size in + onLayout(size) + } } .hideTabBar(props.tabBarHidden) } From 7a0f01bd9f7ab4b4d769965080caaed99ed45b3a Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:02:04 +0200 Subject: [PATCH 27/33] fix: working badges on new tabview --- packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 2dc0b264..bc526879 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -41,7 +41,7 @@ struct NewTabView: AnyTabView { labeled: props.labeled ) } - // .badge(tabData.badge) + .badge((tabData.badge == nil) ? nil : tabData.badge!.isEmpty ? nil : Text(tabData.badge!)) .accessibilityIdentifier(tabData.testID ?? "") } } From 830d04b10f4ecec38261be5a32d8ffe79dcb4f17 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:02:29 +0200 Subject: [PATCH 28/33] chore: run swiftlint --- packages/react-native-bottom-tabs/ios/TabViewImpl.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index c9c9aa49..eed690aa 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -19,17 +19,17 @@ struct TabViewImpl: View { props: props, onLayout: onLayout, onSelect: onSelect - ) { + ) { updateTabBarAppearance(props: props, tabBar: tabBar) - } + } } else { LegacyTabView( props: props, onLayout: onLayout, onSelect: onSelect - ) { + ) { updateTabBarAppearance(props: props, tabBar: tabBar) - } + } } } From 0a127e570d1f1389f4617d694a37e2eecb32956e Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:15:51 +0200 Subject: [PATCH 29/33] chore: remove nil tab role --- packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index bc526879..94661c34 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -17,7 +17,6 @@ struct NewTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] - let role: TabRole? = nil let platformChild = props.children[safe: index] ?? PlatformView() let child = RepresentableView(view: platformChild) @@ -29,7 +28,7 @@ struct NewTabView: AnyTabView { onSelect: onSelect ) - Tab(value: tabData.key, role: role) { + Tab(value: tabData.key) { child .ignoresSafeArea(.container, edges: .all) .tabAppear(using: context) From 1828b18deb9c44df1cb05d1a73d19b23c01a378f Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:18:57 +0200 Subject: [PATCH 30/33] fix: move .tag outside tabItem --- .../react-native-bottom-tabs/ios/TabView/LegacyTabView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index b782f807..209bfaaf 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -46,10 +46,10 @@ struct LegacyTabView: AnyTabView { labeled: props.labeled ) .accessibilityIdentifier(tabData.testID ?? "") - .tag(tabData.key) .tabBadge(tabData.badge) .tabAppear(using: context) } + .tag(tabData.key) } } } From 1bad02236ac40d912575ee764e21ef79997a8411 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Tue, 8 Jul 2025 00:28:57 +0200 Subject: [PATCH 31/33] Merge branch 'main' of https://github.com/padosoft/react-native-bottom-tabs --- .../ios/TabAppearModifier.swift | 16 +- .../ios/TabView/NewTabView.swift | 4 +- .../ios/TabViewImpl.swift | 623 +++++++++--------- 3 files changed, 324 insertions(+), 319 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift index 922b5b5b..58d1855a 100644 --- a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift +++ b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift @@ -13,15 +13,15 @@ struct TabAppearModifier: ViewModifier { func body(content: Content) -> some View { content.onAppear { -#if !os(macOS) - context.updateTabBarAppearance() -#endif + #if !os(macOS) + context.updateTabBarAppearance() + #endif -#if os(iOS) - if context.index >= 4, context.props.selectedPage != context.tabData.key { - context.onSelect(context.tabData.key) - } -#endif + #if os(iOS) + if context.index >= 4, context.props.selectedPage != context.tabData.key { + context.onSelect(context.tabData.key) + } + #endif } } } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 94661c34..b0771d44 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -40,7 +40,9 @@ struct NewTabView: AnyTabView { labeled: props.labeled ) } - .badge((tabData.badge == nil) ? nil : tabData.badge!.isEmpty ? nil : Text(tabData.badge!)) + .badge( + (tabData.badge == nil) ? nil : tabData.badge!.isEmpty ? nil : Text(tabData.badge!) + ) .accessibilityIdentifier(tabData.testID ?? "") } } diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index eed690aa..25481ca9 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -3,353 +3,356 @@ import React import SwiftUI @_spi(Advanced) import SwiftUIIntrospect -/// SwiftUI implementation of TabView used to render React Native views. +/** + SwiftUI implementation of TabView used to render React Native views. + */ struct TabViewImpl: View { - @ObservedObject var props: TabViewProps - #if os(macOS) - @Weak var tabBar: NSTabView? - #else - @Weak var tabBar: UITabBar? - #endif - - @ViewBuilder - var tabContent: some View { - if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { - NewTabView( - props: props, - onLayout: onLayout, - onSelect: onSelect - ) { - updateTabBarAppearance(props: props, tabBar: tabBar) - } - } else { - LegacyTabView( - props: props, - onLayout: onLayout, - onSelect: onSelect - ) { - updateTabBarAppearance(props: props, tabBar: tabBar) - } + @ObservedObject var props: TabViewProps +#if os(macOS) + @Weak var tabBar: NSTabView? +#else + @Weak var tabBar: UITabBar? +#endif + + @ViewBuilder + var tabContent: some View { + if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) { + NewTabView( + props: props, + onLayout: onLayout, + onSelect: onSelect + ) { + updateTabBarAppearance(props: props, tabBar: tabBar) + } + } else { + LegacyTabView( + props: props, + onLayout: onLayout, + onSelect: onSelect + ) { + updateTabBarAppearance(props: props, tabBar: tabBar) + } + } + } + + var onSelect: (_ key: String) -> Void + var onLongPress: (_ key: String) -> Void + var onLayout: (_ size: CGSize) -> Void + var onTabBarMeasured: (_ height: Int) -> Void + + var body: some View { + tabContent + .tabBarMinimizeBehavior(props.minimizeBehavior) +#if !os(tvOS) && !os(macOS) && !os(visionOS) + .onTabItemEvent { index, isLongPress in + let item = props.filteredItems[safe: index] + guard let key = item?.key else { return } + + if isLongPress { + onLongPress(key) + emitHapticFeedback(longPress: true) + } else { + onSelect(key) + emitHapticFeedback() + } + } +#endif + .introspectTabView { tabController in +#if os(macOS) + tabBar = tabController +#else + tabBar = tabController.tabBar + if !props.tabBarHidden { + onTabBarMeasured( + Int(tabController.tabBar.frame.size.height) + ) + } +#endif + } +#if !os(macOS) + .configureAppearance(props: props, tabBar: tabBar) +#endif + .tintColor(props.selectedActiveTintColor) + .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) + .onChange(of: props.selectedPage ?? "") { newValue in +#if !os(macOS) + if props.disablePageAnimations { + UIView.setAnimationsEnabled(false) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + UIView.setAnimationsEnabled(true) } + } +#endif +#if os(tvOS) || os(macOS) || os(visionOS) + onSelect(newValue) +#endif } + } - var onSelect: (_ key: String) -> Void - var onLongPress: (_ key: String) -> Void - var onLayout: (_ size: CGSize) -> Void - var onTabBarMeasured: (_ height: Int) -> Void - - var body: some View { - tabContent - #if !os(tvOS) && !os(macOS) && !os(visionOS) - .onTabItemEvent { index, isLongPress in - let item = props.filteredItems[safe: index] - guard let key = item?.key else { return } - - if isLongPress { - onLongPress(key) - emitHapticFeedback(longPress: true) - } else { - onSelect(key) - emitHapticFeedback() - } - } - #endif - .introspectTabView { tabController in - #if os(macOS) - tabBar = tabController - #else - tabBar = tabController.tabBar - if !props.tabBarHidden { - onTabBarMeasured( - Int(tabController.tabBar.frame.size.height) - ) - } - #endif - } - #if !os(macOS) - .configureAppearance(props: props, tabBar: tabBar) - #endif - .tintColor(props.selectedActiveTintColor) - .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) - .onChange(of: props.selectedPage ?? "") { newValue in - #if !os(macOS) - if props.disablePageAnimations { - UIView.setAnimationsEnabled(false) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UIView.setAnimationsEnabled(true) - } - } - #endif - #if os(tvOS) || os(macOS) || os(visionOS) - onSelect(newValue) - #endif - } + func emitHapticFeedback(longPress: Bool = false) { +#if os(iOS) + if !props.hapticFeedbackEnabled { + return } - func emitHapticFeedback(longPress: Bool = false) { - #if os(iOS) - if !props.hapticFeedbackEnabled { - return - } - - if longPress { - UINotificationFeedbackGenerator().notificationOccurred(.success) - } else { - UISelectionFeedbackGenerator().selectionChanged() - } - #endif + if longPress { + UINotificationFeedbackGenerator().notificationOccurred(.success) + } else { + UISelectionFeedbackGenerator().selectionChanged() } +#endif + } } #if !os(macOS) - private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { - guard let tabBar else { return } +private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { + guard let tabBar else { return } - tabBar.isHidden = props.tabBarHidden + tabBar.isHidden = props.tabBarHidden - if props.scrollEdgeAppearance == "transparent" { - configureTransparentAppearance(tabBar: tabBar, props: props) - return - } + if props.scrollEdgeAppearance == "transparent" { + configureTransparentAppearance(tabBar: tabBar, props: props) + return + } - configureStandardAppearance(tabBar: tabBar, props: props) - } + configureStandardAppearance(tabBar: tabBar, props: props) +} #endif private func createFontAttributes( - size: CGFloat, - family: String?, - weight: String?, - inactiveTintColor: PlatformColor? + size: CGFloat, + family: String?, + weight: String?, + inactiveTintColor: PlatformColor? ) -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] = [:] - - if family != nil || weight != nil { - attributes[.font] = RCTFont.update( - nil, - withFamily: family, - size: NSNumber(value: size), - weight: weight, - style: nil, - variant: nil, - scaleMultiplier: 1.0 - ) - } else { - attributes[.font] = UIFont.boldSystemFont(ofSize: size) - } - - return attributes + var attributes: [NSAttributedString.Key: Any] = [:] + + if family != nil || weight != nil { + attributes[.font] = RCTFont.update( + nil, + withFamily: family, + size: NSNumber(value: size), + weight: weight, + style: nil, + variant: nil, + scaleMultiplier: 1.0 + ) + } else { + attributes[.font] = UIFont.boldSystemFont(ofSize: size) + } + + return attributes } #if os(tvOS) - let tabBarDefaultFontSize: CGFloat = 30.0 +let tabBarDefaultFontSize: CGFloat = 30.0 #else - let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize +let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize #endif #if !os(macOS) - private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { - tabBar.barTintColor = props.barTintColor - #if !os(visionOS) - tabBar.isTranslucent = props.translucent - #endif - tabBar.unselectedItemTintColor = props.inactiveTintColor - - guard let items = tabBar.items else { return } - - let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize - let attributes = createFontAttributes( - size: fontSize, - family: props.fontFamily, - weight: props.fontWeight, - inactiveTintColor: nil - ) - - items.forEach { item in - item.setTitleTextAttributes(attributes, for: .normal) - } - } - - private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { - let appearance = UITabBarAppearance() - - // Configure background - switch props.scrollEdgeAppearance { - case "opaque": - appearance.configureWithOpaqueBackground() - default: - appearance.configureWithDefaultBackground() - } - - if props.translucent == false { - appearance.configureWithOpaqueBackground() - } - - if props.barTintColor != nil { - appearance.backgroundColor = props.barTintColor - } - - // Configure item appearance - let itemAppearance = UITabBarItemAppearance() - let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize +private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { + tabBar.barTintColor = props.barTintColor +#if !os(visionOS) + tabBar.isTranslucent = props.translucent +#endif + tabBar.unselectedItemTintColor = props.inactiveTintColor - var attributes = createFontAttributes( - size: fontSize, - family: props.fontFamily, - weight: props.fontWeight, - inactiveTintColor: props.inactiveTintColor - ) + guard let items = tabBar.items else { return } - if let inactiveTintColor = props.inactiveTintColor { - attributes[.foregroundColor] = inactiveTintColor - } - - if let inactiveTintColor = props.inactiveTintColor { - itemAppearance.normal.iconColor = inactiveTintColor - } + let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize + let attributes = createFontAttributes( + size: fontSize, + family: props.fontFamily, + weight: props.fontWeight, + inactiveTintColor: nil + ) - itemAppearance.normal.titleTextAttributes = attributes - - // Apply item appearance to all layouts - appearance.stackedLayoutAppearance = itemAppearance - appearance.inlineLayoutAppearance = itemAppearance - appearance.compactInlineLayoutAppearance = itemAppearance + items.forEach { item in + item.setTitleTextAttributes(attributes, for: .normal) + } +} - // Apply final appearance - tabBar.standardAppearance = appearance - if #available(iOS 15.0, *) { - tabBar.scrollEdgeAppearance = appearance.copy() - } - } +private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { + let appearance = UITabBarAppearance() + + // Configure background + switch props.scrollEdgeAppearance { + case "opaque": + appearance.configureWithOpaqueBackground() + default: + appearance.configureWithDefaultBackground() + } + + if props.translucent == false { + appearance.configureWithOpaqueBackground() + } + + if props.barTintColor != nil { + appearance.backgroundColor = props.barTintColor + } + + // Configure item appearance + let itemAppearance = UITabBarItemAppearance() + let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize + + var attributes = createFontAttributes( + size: fontSize, + family: props.fontFamily, + weight: props.fontWeight, + inactiveTintColor: props.inactiveTintColor + ) + + if let inactiveTintColor = props.inactiveTintColor { + attributes[.foregroundColor] = inactiveTintColor + } + + if let inactiveTintColor = props.inactiveTintColor { + itemAppearance.normal.iconColor = inactiveTintColor + } + + itemAppearance.normal.titleTextAttributes = attributes + + // Apply item appearance to all layouts + appearance.stackedLayoutAppearance = itemAppearance + appearance.inlineLayoutAppearance = itemAppearance + appearance.compactInlineLayoutAppearance = itemAppearance + + // Apply final appearance + tabBar.standardAppearance = appearance + if #available(iOS 15.0, *) { + tabBar.scrollEdgeAppearance = appearance.copy() + } +} #endif extension View { - @ViewBuilder - func getSidebarAdaptable(enabled: Bool) -> some View { - if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) { - if enabled { - #if compiler(>=6.0) - self.tabViewStyle(.sidebarAdaptable) - #else - self - #endif - } else { - self - } - } else { - self - } + @ViewBuilder + func getSidebarAdaptable(enabled: Bool) -> some View { + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) { + if enabled { +#if compiler(>=6.0) + self.tabViewStyle(.sidebarAdaptable) +#else + self +#endif + } else { + self + } + } else { + self } - - @ViewBuilder - func tabBadge(_ data: String?) -> some View { - if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) { - if let data { - #if !os(tvOS) - self.badge(data) - #else - self - #endif - } else { - self - } - } else { - self - } + } + + @ViewBuilder + func tabBadge(_ data: String?) -> some View { + if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) { + if let data { +#if !os(tvOS) + self.badge(data) +#else + self +#endif + } else { + self + } + } else { + self } + } - #if !os(macOS) - @ViewBuilder - func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View { - self - .onChange(of: props.barTintColor) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.scrollEdgeAppearance) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.translucent) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.inactiveTintColor) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.selectedActiveTintColor) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.fontSize) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.fontFamily) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.fontWeight) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) - } - .onChange(of: props.tabBarHidden) { newValue in - tabBar?.isHidden = newValue - } - } - #endif - - @ViewBuilder - func tintColor(_ color: PlatformColor?) -> some View { - if let color { - let color = Color(color) - if #available(iOS 16.0, tvOS 16.0, macOS 13.0, *) { - self.tint(color) - } else { - self.accentColor(color) - } - } else { - self - } - } +#if !os(macOS) + @ViewBuilder + func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View { + self + .onChange(of: props.barTintColor) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.scrollEdgeAppearance) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.translucent) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.inactiveTintColor) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.selectedActiveTintColor) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontSize) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontFamily) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontWeight) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.tabBarHidden) { newValue in + tabBar?.isHidden = newValue + } + } +#endif - @ViewBuilder - func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View { - #if compiler(>=6.2) - if #available(iOS 26.0, *) { - if let behavior { - self.tabBarMinimizeBehavior(behavior.convert()) - } else { - self - } - } else { - self - } - #else - self - #endif + @ViewBuilder + func tintColor(_ color: PlatformColor?) -> some View { + if let color { + let color = Color(color) + if #available(iOS 16.0, tvOS 16.0, macOS 13.0, *) { + self.tint(color) + } else { + self.accentColor(color) + } + } else { + self } - - @ViewBuilder - func hideTabBar(_ flag: Bool) -> some View { - #if !os(macOS) - if flag { - if #available(iOS 16.0, tvOS 16.0, *) { - self.toolbar(.hidden, for: .tabBar) - } else { - // We fallback to isHidden on UITabBar - self - } - } else { - self - } - #else - self - #endif + } + + @ViewBuilder + func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View { +#if compiler(>=6.2) + if #available(iOS 26.0, *) { + if let behavior { + self.tabBarMinimizeBehavior(behavior.convert()) + } else { + self + } + } else { + self } +#else + self +#endif + } - // Allows TabView to use unfilled SFSymbols. - // By default they are always filled. - @ViewBuilder - func noneSymbolVariant() -> some View { - if #available(iOS 15.0, tvOS 15.0, macOS 13.0, *) { - self - .environment(\.symbolVariants, .none) - } else { - self - } + @ViewBuilder + func hideTabBar(_ flag: Bool) -> some View { +#if !os(macOS) + if flag { + if #available(iOS 16.0, tvOS 16.0, *) { + self.toolbar(.hidden, for: .tabBar) + } else { + // We fallback to isHidden on UITabBar + self + } + } else { + self + } +#else + self +#endif + } + + // Allows TabView to use unfilled SFSymbols. + // By default they are always filled. + @ViewBuilder + func noneSymbolVariant() -> some View { + if #available(iOS 15.0, tvOS 15.0, macOS 13.0, *) { + self + .environment(\.symbolVariants, .none) + } else { + self } + } } From e8c9ed2d69afa99c7c745d2d0012a96a0d7890c1 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:20:37 +0200 Subject: [PATCH 32/33] =?UTF-8?q?=C2=A0fix:=20move=20.tab=20modifiers=20in?= =?UTF-8?q?=20LegacyTabView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react-native-bottom-tabs/ios/TabView/LegacyTabView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 209bfaaf..6464986b 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -46,10 +46,10 @@ struct LegacyTabView: AnyTabView { labeled: props.labeled ) .accessibilityIdentifier(tabData.testID ?? "") - .tabBadge(tabData.badge) - .tabAppear(using: context) } .tag(tabData.key) + .tabBadge(tabData.badge) + .tabAppear(using: context) } } } From bbcb542378f71c4cbdaf1f83c88495acbd0377e1 Mon Sep 17 00:00:00 2001 From: PADO <62028267+47PADO47@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:20:45 +0200 Subject: [PATCH 33/33] refactor: update NewTabView .badge --- .../react-native-bottom-tabs/ios/TabView/NewTabView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index b0771d44..01754e79 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -40,9 +40,7 @@ struct NewTabView: AnyTabView { labeled: props.labeled ) } - .badge( - (tabData.badge == nil) ? nil : tabData.badge!.isEmpty ? nil : Text(tabData.badge!) - ) + .badge(tabData.badge.flatMap { !$0.isEmpty ? Text($0) : nil }) .accessibilityIdentifier(tabData.testID ?? "") } }