@soundtrackio/react-native-bottom-tabs 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +32 -0
  2. package/android/build.gradle +132 -0
  3. package/android/gradle.properties +5 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/AndroidManifestNew.xml +2 -0
  6. package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
  7. package/android/src/main/java/com/rcttabview/LatestRequestTracker.kt +19 -0
  8. package/android/src/main/java/com/rcttabview/RCTTabView.kt +689 -0
  9. package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +222 -0
  10. package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +19 -0
  11. package/android/src/main/java/com/rcttabview/RCTTabViewPrewarm.kt +40 -0
  12. package/android/src/main/java/com/rcttabview/RebuildBatch.kt +31 -0
  13. package/android/src/main/java/com/rcttabview/TabBarVisibility.kt +14 -0
  14. package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
  15. package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +25 -0
  16. package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +24 -0
  17. package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +32 -0
  18. package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +24 -0
  19. package/android/src/test/java/com/rcttabview/LatestRequestTrackerTest.kt +37 -0
  20. package/android/src/test/java/com/rcttabview/RebuildBatchTest.kt +44 -0
  21. package/android/src/test/java/com/rcttabview/TabBarVisibilityTest.kt +58 -0
  22. package/app.plugin.js +1 -0
  23. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +33 -0
  24. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +19 -0
  25. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
  26. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
  27. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
  28. package/ios/BottomAccessoryProvider.swift +27 -0
  29. package/ios/Extensions.swift +101 -0
  30. package/ios/PlatformAliases.swift +43 -0
  31. package/ios/RCTBottomAccessoryComponentView.h +17 -0
  32. package/ios/RCTBottomAccessoryComponentView.mm +76 -0
  33. package/ios/RCTTabViewComponentView.h +16 -0
  34. package/ios/RCTTabViewComponentView.mm +285 -0
  35. package/ios/RepresentableView.swift +55 -0
  36. package/ios/SVG/CoreSVG.h +19 -0
  37. package/ios/SVG/CoreSVG.mm +177 -0
  38. package/ios/SVG/SvgDecoder.h +10 -0
  39. package/ios/SVG/SvgDecoder.mm +38 -0
  40. package/ios/TabAppearModifier.swift +38 -0
  41. package/ios/TabBarFontSize.swift +88 -0
  42. package/ios/TabItem.swift +35 -0
  43. package/ios/TabItemEventModifier.swift +163 -0
  44. package/ios/TabView/AnyTabView.swift +7 -0
  45. package/ios/TabView/LegacyTabView.swift +70 -0
  46. package/ios/TabView/NewTabView.swift +138 -0
  47. package/ios/TabViewImpl.swift +566 -0
  48. package/ios/TabViewProps.swift +109 -0
  49. package/ios/TabViewProvider.swift +368 -0
  50. package/ios/react-native-bottom-tabs-Bridging-Header.h +4 -0
  51. package/lib/module/BottomAccessoryView.js +47 -0
  52. package/lib/module/BottomAccessoryView.js.map +1 -0
  53. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  54. package/lib/module/DelayedFreeze.js +31 -0
  55. package/lib/module/DelayedFreeze.js.map +1 -0
  56. package/lib/module/NativeSVGDecoder.js +5 -0
  57. package/lib/module/NativeSVGDecoder.js.map +1 -0
  58. package/lib/module/SceneMap.js +28 -0
  59. package/lib/module/SceneMap.js.map +1 -0
  60. package/lib/module/TabView.js +294 -0
  61. package/lib/module/TabView.js.map +1 -0
  62. package/lib/module/TabViewNativeComponent.ts +73 -0
  63. package/lib/module/expo.js +35 -0
  64. package/lib/module/expo.js.map +1 -0
  65. package/lib/module/index.js +20 -0
  66. package/lib/module/index.js.map +1 -0
  67. package/lib/module/package.json +1 -0
  68. package/lib/module/types.js +4 -0
  69. package/lib/module/types.js.map +1 -0
  70. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  71. package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
  72. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  73. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/BottomAccessoryView.d.ts +7 -0
  76. package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
  77. package/lib/typescript/src/BottomAccessoryView.js +24 -0
  78. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +15 -0
  79. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
  80. package/lib/typescript/src/BottomAccessoryViewNativeComponent.js +4 -0
  81. package/lib/typescript/src/DelayedFreeze.d.ts +7 -0
  82. package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
  83. package/lib/typescript/src/DelayedFreeze.js +21 -0
  84. package/lib/typescript/src/NativeSVGDecoder.d.ts +5 -0
  85. package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeSVGDecoder.js +2 -0
  87. package/lib/typescript/src/SceneMap.d.ts +9 -0
  88. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  89. package/lib/typescript/src/SceneMap.js +15 -0
  90. package/lib/typescript/src/TabView.d.ts +219 -0
  91. package/lib/typescript/src/TabView.d.ts.map +1 -0
  92. package/lib/typescript/src/TabView.js +157 -0
  93. package/lib/typescript/src/TabViewNativeComponent.d.ts +59 -0
  94. package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
  95. package/lib/typescript/src/TabViewNativeComponent.js +4 -0
  96. package/lib/typescript/src/expo.d.ts +6 -0
  97. package/lib/typescript/src/expo.d.ts.map +1 -0
  98. package/lib/typescript/src/expo.js +34 -0
  99. package/lib/typescript/src/index.d.ts +15 -0
  100. package/lib/typescript/src/index.d.ts.map +1 -0
  101. package/lib/typescript/src/index.js +11 -0
  102. package/lib/typescript/src/types.d.ts +31 -0
  103. package/lib/typescript/src/types.d.ts.map +1 -0
  104. package/lib/typescript/src/types.js +1 -0
  105. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +2 -0
  106. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
  107. package/lib/typescript/src/utils/BottomTabBarHeightContext.js +2 -0
  108. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +1 -0
  109. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
  110. package/lib/typescript/src/utils/useBottomTabBarHeight.js +9 -0
  111. package/lib/typescript/tsconfig.tsbuildinfo +1 -0
  112. package/package.json +135 -0
  113. package/react-native-bottom-tabs.podspec +43 -0
  114. package/src/BottomAccessoryView.tsx +59 -0
  115. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  116. package/src/DelayedFreeze.tsx +31 -0
  117. package/src/NativeSVGDecoder.ts +5 -0
  118. package/src/SceneMap.tsx +34 -0
  119. package/src/TabView.tsx +558 -0
  120. package/src/TabViewNativeComponent.ts +73 -0
  121. package/src/expo.ts +58 -0
  122. package/src/index.tsx +23 -0
  123. package/src/types.ts +36 -0
  124. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  125. package/src/utils/useBottomTabBarHeight.ts +15 -0
@@ -0,0 +1,138 @@
1
+ import React
2
+ import SwiftUI
3
+
4
+ @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
5
+ struct NewTabView: AnyTabView {
6
+ @ObservedObject var props: TabViewProps
7
+
8
+ var onLayout: (CGSize) -> Void
9
+ var onSelect: (String) -> Void
10
+ var updateTabBarAppearance: () -> Void
11
+
12
+ private var effectiveLayoutDirection: LayoutDirection {
13
+ let dir = props.layoutDirection ?? "locale"
14
+ if let mapped = ["rtl": LayoutDirection.rightToLeft,
15
+ "ltr": LayoutDirection.leftToRight][dir] {
16
+ return mapped
17
+ }
18
+ let system = UIView.userInterfaceLayoutDirection(for: .unspecified)
19
+ return system == .rightToLeft ? .rightToLeft : .leftToRight
20
+ }
21
+
22
+ @ViewBuilder
23
+ var body: some View {
24
+ TabView(selection: $props.selectedPage) {
25
+ ForEach(props.children) { child in
26
+ if let index = props.children.firstIndex(of: child),
27
+ let tabData = props.items[safe: index] {
28
+ let isFocused = props.selectedPage == tabData.key
29
+
30
+ if !tabData.hidden || isFocused {
31
+ let icon = props.icons[index]
32
+
33
+ let context = TabAppearContext(
34
+ index: index,
35
+ tabData: tabData,
36
+ props: props,
37
+ updateTabBarAppearance: updateTabBarAppearance,
38
+ onSelect: onSelect
39
+ )
40
+
41
+ Tab(value: tabData.key, role: tabData.role?.convert()) {
42
+ RepresentableView(view: child.view)
43
+ .ignoresSafeArea(.container, edges: .all)
44
+ .tabAppear(using: context)
45
+ .hideTabBar(props.tabBarHidden)
46
+ } label: {
47
+ TabItem(
48
+ title: tabData.title,
49
+ icon: icon,
50
+ sfSymbol: tabData.sfSymbol,
51
+ labeled: props.labeled,
52
+ iconRenderingMode: tabData.iconRenderingMode
53
+ )
54
+ }
55
+ #if !os(tvOS)
56
+ .badge(tabData.badge.flatMap { !$0.isEmpty ? Text($0) : nil })
57
+ #endif
58
+ .accessibilityIdentifier(tabData.testID ?? "")
59
+ }
60
+ }
61
+ }
62
+ }
63
+ .environment(\.layoutDirection, effectiveLayoutDirection)
64
+ .measureView { size in
65
+ onLayout(size)
66
+ }
67
+ .modifier(ConditionalBottomAccessoryModifier(props: props))
68
+ }
69
+ }
70
+
71
+ struct ConditionalBottomAccessoryModifier: ViewModifier {
72
+ @ObservedObject var props: TabViewProps
73
+
74
+ private var bottomAccessoryView: PlatformView? {
75
+ props.children.first { child in
76
+ let className = String(describing: type(of: child.view))
77
+ return className == "RCTBottomAccessoryComponentView"
78
+ }?.view
79
+ }
80
+
81
+ func body(content: Content) -> some View {
82
+ #if os(macOS) || os(tvOS) || !compiler(>=6.2)
83
+ // tabViewBottomAccessory is not available on macOS
84
+ content
85
+ #else
86
+ if #available(iOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
87
+ content
88
+ .tabViewBottomAccessory {
89
+ renderBottomAccessoryView()
90
+ }
91
+ } else {
92
+ content
93
+ }
94
+ #endif
95
+ }
96
+
97
+ @ViewBuilder
98
+ private func renderBottomAccessoryView() -> some View {
99
+ #if !os(macOS) && !os(tvOS) && compiler(>=6.2)
100
+ if let bottomAccessoryView {
101
+ if #available(iOS 26.0, *) {
102
+ BottomAccessoryRepresentableView(view: bottomAccessoryView)
103
+ }
104
+ }
105
+ #endif
106
+ }
107
+ }
108
+
109
+ #if !os(macOS) && !os(tvOS) && compiler(>=6.2)
110
+ @available(iOS 26.0, tvOS 26.0, *)
111
+ struct BottomAccessoryRepresentableView: PlatformViewRepresentable {
112
+ @Environment(\.tabViewBottomAccessoryPlacement) var tabViewBottomAccessoryPlacement
113
+ var view: PlatformView
114
+
115
+ func makeUIView(context: Context) -> PlatformView {
116
+ let wrapper = UIView()
117
+ wrapper.addSubview(view)
118
+
119
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
120
+
121
+ emitPlacementChanged(for: view)
122
+ return wrapper
123
+ }
124
+
125
+ func updateUIView(_ uiView: PlatformView, context: Context) {
126
+ if let subview = uiView.subviews.first {
127
+ subview.frame = uiView.bounds
128
+ }
129
+ emitPlacementChanged(for: view)
130
+ }
131
+
132
+ private func emitPlacementChanged(for uiView: PlatformView) {
133
+ if let contentView = uiView.value(forKey: "bottomAccessoryProvider") as? BottomAccessoryProvider {
134
+ contentView.emitPlacementChanged(tabViewBottomAccessoryPlacement)
135
+ }
136
+ }
137
+ }
138
+ #endif
@@ -0,0 +1,566 @@
1
+ import Foundation
2
+ import React
3
+ import SwiftUI
4
+ @_spi(Advanced) import SwiftUIIntrospect
5
+
6
+ /// SwiftUI implementation of TabView used to render React Native views.
7
+ struct TabViewImpl: View {
8
+ @ObservedObject var props: TabViewProps
9
+ #if os(macOS)
10
+ @Weak var tabBar: NSTabView?
11
+ #else
12
+ @Weak var tabBar: UITabBar?
13
+ #endif
14
+
15
+ @ViewBuilder
16
+ var tabContent: some View {
17
+ if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) {
18
+ NewTabView(
19
+ props: props,
20
+ onLayout: onLayout,
21
+ onSelect: onSelect
22
+ ) {
23
+ #if !os(macOS)
24
+ updateTabBarAppearance(props: props, tabBar: tabBar)
25
+ #endif
26
+ }
27
+ } else {
28
+ LegacyTabView(
29
+ props: props,
30
+ onLayout: onLayout,
31
+ onSelect: onSelect
32
+ ) {
33
+ #if !os(macOS)
34
+ updateTabBarAppearance(props: props, tabBar: tabBar)
35
+ #endif
36
+ }
37
+ }
38
+ }
39
+
40
+ var onSelect: (_ key: String) -> Void
41
+ var onLongPress: (_ key: String) -> Void
42
+ var onLayout: (_ size: CGSize) -> Void
43
+ var onTabBarMeasured: (_ height: Int) -> Void
44
+
45
+ var body: some View {
46
+ tabContent
47
+ .tabBarMinimizeBehavior(props.minimizeBehavior)
48
+ #if !os(tvOS) && !os(macOS) && !os(visionOS)
49
+ .onTabItemEvent { index, identifier, isLongPress in
50
+ let item = identifier.flatMap { props.filteredItems.findByKey($0) }
51
+ ?? index.flatMap { props.filteredItems[safe: $0] }
52
+ guard let key = item?.key else { return false }
53
+
54
+ if isLongPress {
55
+ onLongPress(key)
56
+ emitHapticFeedback(longPress: true)
57
+ } else {
58
+ onSelect(key)
59
+ emitHapticFeedback()
60
+ }
61
+ return item?.preventsDefault ?? false
62
+ }
63
+ #endif
64
+ .introspectTabView { tabController in
65
+ #if !os(macOS)
66
+ tabController.view.backgroundColor = .clear
67
+ tabController.viewControllers?.forEach { $0.view.backgroundColor = .clear }
68
+ #endif
69
+ #if os(macOS)
70
+ tabBar = tabController
71
+ #else
72
+ tabBar = tabController.tabBar
73
+ updateTabBarAppearance(props: props, tabBar: tabController.tabBar)
74
+ updateTabBarItemImages(props: props, tabBar: tabController.tabBar)
75
+ if !props.tabBarHidden {
76
+ onTabBarMeasured(
77
+ Int(tabController.tabBar.frame.size.height)
78
+ )
79
+ }
80
+ #endif
81
+ }
82
+ #if !os(macOS)
83
+ .configureAppearance(props: props, tabBar: tabBar)
84
+ #endif
85
+ .tintColor(props.selectedActiveTintColor)
86
+ .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false)
87
+ .onChange(of: props.selectedPage ?? "") { newValue in
88
+ #if !os(macOS)
89
+ if props.disablePageAnimations {
90
+ UIView.setAnimationsEnabled(false)
91
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
92
+ UIView.setAnimationsEnabled(true)
93
+ }
94
+ }
95
+ #endif
96
+ #if os(tvOS) || os(macOS) || os(visionOS)
97
+ onSelect(newValue)
98
+ #endif
99
+ }
100
+ }
101
+
102
+ func emitHapticFeedback(longPress: Bool = false) {
103
+ #if os(iOS)
104
+ if !props.hapticFeedbackEnabled {
105
+ return
106
+ }
107
+
108
+ if longPress {
109
+ UINotificationFeedbackGenerator().notificationOccurred(.success)
110
+ } else {
111
+ UISelectionFeedbackGenerator().selectionChanged()
112
+ }
113
+ #endif
114
+ }
115
+ }
116
+
117
+ #if !os(macOS)
118
+ private func updateTabBarItemImages(props: TabViewProps, tabBar: UITabBar?) {
119
+ guard let tabBar,
120
+ let items = tabBar.items
121
+ else { return }
122
+
123
+ configureTabBarItemImages(items: items, props: props)
124
+
125
+ DispatchQueue.main.async { [weak tabBar] in
126
+ guard let tabBar, let items = tabBar.items else { return }
127
+ configureTabBarItemImages(items: items, props: props)
128
+ }
129
+ }
130
+
131
+ private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) {
132
+ guard let tabBar else { return }
133
+
134
+ tabBar.isHidden = props.tabBarHidden
135
+
136
+ if props.scrollEdgeAppearance == "transparent" {
137
+ configureTransparentAppearance(tabBar: tabBar, props: props)
138
+ return
139
+ }
140
+
141
+ configureStandardAppearance(tabBar: tabBar, props: props)
142
+ }
143
+ #endif
144
+
145
+ #if !os(macOS)
146
+ private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) {
147
+ tabBar.barTintColor = props.barTintColor
148
+ tabBar.tintColor = props.selectedActiveTintColor
149
+ #if !os(visionOS)
150
+ tabBar.isTranslucent = props.translucent
151
+ #endif
152
+ tabBar.unselectedItemTintColor = props.effectiveInactiveTintColor
153
+
154
+ guard let items = tabBar.items else { return }
155
+
156
+ let fontAttributes = TabBarFontSize.createNormalStateAttributes(
157
+ fontSize: props.fontSize,
158
+ fontFamily: props.fontFamily,
159
+ fontWeight: props.fontWeight,
160
+ inactiveColor: nil
161
+ )
162
+
163
+ items.forEach { item in
164
+ item.setTitleTextAttributes(fontAttributes, for: .normal)
165
+ item.setTitleTextAttributes(selectedAttributes(props: props), for: .selected)
166
+ }
167
+ }
168
+
169
+ private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) {
170
+ let appearance = UITabBarAppearance()
171
+ tabBar.tintColor = props.selectedActiveTintColor
172
+ tabBar.unselectedItemTintColor = props.effectiveInactiveTintColor
173
+
174
+ // Configure background
175
+ switch props.scrollEdgeAppearance {
176
+ case "opaque":
177
+ appearance.configureWithOpaqueBackground()
178
+ default:
179
+ appearance.configureWithDefaultBackground()
180
+ }
181
+
182
+ if props.translucent == false {
183
+ appearance.configureWithOpaqueBackground()
184
+ }
185
+
186
+ if props.barTintColor != nil {
187
+ appearance.backgroundColor = props.barTintColor
188
+ }
189
+
190
+ // Configure item appearance
191
+ let itemAppearance = UITabBarItemAppearance()
192
+
193
+ let attributes = TabBarFontSize.createNormalStateAttributes(
194
+ fontSize: props.fontSize,
195
+ fontFamily: props.fontFamily,
196
+ fontWeight: props.fontWeight,
197
+ inactiveColor: props.effectiveInactiveTintColor
198
+ )
199
+
200
+ if let inactiveTintColor = props.effectiveInactiveTintColor {
201
+ itemAppearance.normal.iconColor = inactiveTintColor
202
+ }
203
+ if let activeTintColor = props.selectedActiveTintColor {
204
+ itemAppearance.selected.iconColor = activeTintColor
205
+ }
206
+
207
+ itemAppearance.normal.titleTextAttributes = attributes
208
+ itemAppearance.selected.titleTextAttributes = selectedAttributes(props: props)
209
+
210
+ // Apply item appearance to all layouts
211
+ appearance.stackedLayoutAppearance = itemAppearance
212
+ appearance.inlineLayoutAppearance = itemAppearance
213
+ appearance.compactInlineLayoutAppearance = itemAppearance
214
+
215
+ // Apply final appearance
216
+ tabBar.standardAppearance = appearance
217
+ if #available(iOS 15.0, *) {
218
+ tabBar.scrollEdgeAppearance = appearance.copy()
219
+ }
220
+ }
221
+
222
+ private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) {
223
+ for (tabBarIndex, item) in items.enumerated() {
224
+ guard let tabData = props.filteredItems[safe: tabBarIndex],
225
+ let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key })
226
+ else { continue }
227
+
228
+ let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor
229
+ let assetIcon = props.icons[itemIndex]
230
+ let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol)
231
+ let focusedIcon =
232
+ props.focusedIcons[itemIndex] ?? makeSFSymbolImage(named: tabData.focusedSfSymbol) ?? icon
233
+ let preservesOriginalIconColors = preservesOriginalIconColors(tabData: tabData)
234
+ let useBakedTintColors = shouldUseExperimentalBakedTintColors(props: props)
235
+ let shouldRenderLabelIntoImage =
236
+ props.hasCustomTintColors && props.labeled && tabData.role != .search && icon != nil
237
+
238
+ item.accessibilityLabel = tabData.title
239
+
240
+ if useBakedTintColors, shouldRenderLabelIntoImage, let icon {
241
+ let selectedIcon = focusedIcon ?? icon
242
+ item.title = ""
243
+ item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 100)
244
+ item.image = makeTabBarItemImage(
245
+ icon: icon,
246
+ title: tabData.title,
247
+ color: props.inactiveTintColor,
248
+ preservesOriginalIconColors: preservesOriginalIconColors,
249
+ props: props
250
+ )
251
+ item.selectedImage = makeTabBarItemImage(
252
+ icon: selectedIcon,
253
+ title: tabData.title,
254
+ color: tabActiveColor,
255
+ preservesOriginalIconColors: preservesOriginalIconColors,
256
+ props: props
257
+ )
258
+ continue
259
+ }
260
+
261
+ item.title = props.labeled ? tabData.title : nil
262
+ item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 0)
263
+
264
+ if let icon {
265
+ let selectedIcon = focusedIcon ?? icon
266
+ item.image = renderTabBarIcon(
267
+ icon,
268
+ color: props.inactiveTintColor,
269
+ preservesOriginalIconColors: preservesOriginalIconColors,
270
+ forceTintColor: useBakedTintColors
271
+ )
272
+ item.selectedImage = renderTabBarIcon(
273
+ selectedIcon,
274
+ color: tabActiveColor,
275
+ preservesOriginalIconColors: preservesOriginalIconColors,
276
+ forceTintColor: useBakedTintColors
277
+ )
278
+ }
279
+
280
+ item.setTitleTextAttributes(
281
+ TabBarFontSize.createFontAttributes(
282
+ size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize,
283
+ family: props.fontFamily,
284
+ weight: props.fontWeight,
285
+ color: tabActiveColor
286
+ ),
287
+ for: .selected
288
+ )
289
+ }
290
+ }
291
+
292
+ private func preservesOriginalIconColors(tabData: TabInfo) -> Bool {
293
+ tabData.iconRenderingMode == "original"
294
+ }
295
+
296
+ private func renderTabBarIcon(
297
+ _ icon: UIImage,
298
+ color: UIColor?,
299
+ preservesOriginalIconColors: Bool,
300
+ forceTintColor: Bool
301
+ ) -> UIImage {
302
+ if preservesOriginalIconColors {
303
+ return icon.withRenderingMode(.alwaysOriginal)
304
+ }
305
+
306
+ guard forceTintColor, let color else {
307
+ return icon
308
+ }
309
+
310
+ return icon.withTintColor(color, renderingMode: .alwaysOriginal)
311
+ }
312
+
313
+ private func makeSFSymbolImage(named sfSymbol: String?) -> UIImage? {
314
+ guard let sfSymbol, !sfSymbol.isEmpty else { return nil }
315
+
316
+ return UIImage(systemName: sfSymbol)
317
+ }
318
+
319
+ private func selectedAttributes(props: TabViewProps) -> [NSAttributedString.Key: Any] {
320
+ TabBarFontSize.createFontAttributes(
321
+ size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize,
322
+ family: props.fontFamily,
323
+ weight: props.fontWeight,
324
+ color: props.selectedActiveTintColor
325
+ )
326
+ }
327
+
328
+ private func shouldUseExperimentalBakedTintColors(props: TabViewProps) -> Bool {
329
+ guard props.experimentalBakedTintColors else {
330
+ return false
331
+ }
332
+
333
+ #if os(iOS)
334
+ if #available(iOS 26.0, *) {
335
+ return true
336
+ }
337
+ #endif
338
+
339
+ return false
340
+ }
341
+
342
+ private func makeTabBarItemImage(
343
+ icon: UIImage,
344
+ title: String,
345
+ color: UIColor?,
346
+ preservesOriginalIconColors: Bool = false,
347
+ props: TabViewProps
348
+ ) -> UIImage {
349
+ let color = color ?? .label
350
+ let iconSize = CGSize(width: 27, height: 27)
351
+ let font =
352
+ TabBarFontSize.createFontAttributes(
353
+ size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize,
354
+ family: props.fontFamily,
355
+ weight: props.fontWeight
356
+ )[.font] as? UIFont ?? UIFont.boldSystemFont(ofSize: TabBarFontSize.defaultSize)
357
+ let paragraphStyle = NSMutableParagraphStyle()
358
+ paragraphStyle.alignment = .center
359
+ let attributes: [NSAttributedString.Key: Any] = [
360
+ .font: font,
361
+ .foregroundColor: color,
362
+ .paragraphStyle: paragraphStyle,
363
+ ]
364
+ let titleSize = (title as NSString).size(withAttributes: attributes)
365
+ let imageSize = CGSize(
366
+ width: max(iconSize.width, ceil(titleSize.width)) + 8,
367
+ height: iconSize.height + 3 + ceil(titleSize.height)
368
+ )
369
+ let format = UIGraphicsImageRendererFormat()
370
+ format.scale = UIScreen.main.scale
371
+
372
+ let image = UIGraphicsImageRenderer(size: imageSize, format: format).image { _ in
373
+ let tintedIcon: UIImage
374
+ if preservesOriginalIconColors {
375
+ tintedIcon = icon.withRenderingMode(.alwaysOriginal)
376
+ } else {
377
+ tintedIcon = icon.withTintColor(color, renderingMode: .alwaysOriginal)
378
+ }
379
+ let iconFrame = aspectFitRect(
380
+ size: tintedIcon.size,
381
+ in: CGRect(
382
+ x: (imageSize.width - iconSize.width) / 2,
383
+ y: 0,
384
+ width: iconSize.width,
385
+ height: iconSize.height
386
+ )
387
+ )
388
+
389
+ tintedIcon.draw(in: iconFrame)
390
+
391
+ (title as NSString).draw(
392
+ in: CGRect(
393
+ x: 0,
394
+ y: iconSize.height + 3,
395
+ width: imageSize.width,
396
+ height: ceil(titleSize.height)
397
+ ),
398
+ withAttributes: attributes
399
+ )
400
+ }
401
+
402
+ return image.withRenderingMode(.alwaysOriginal)
403
+ }
404
+
405
+ private func aspectFitRect(size: CGSize, in rect: CGRect) -> CGRect {
406
+ guard size.width > 0, size.height > 0 else {
407
+ return rect
408
+ }
409
+
410
+ let scale = min(rect.width / size.width, rect.height / size.height)
411
+ let fittedSize = CGSize(width: size.width * scale, height: size.height * scale)
412
+
413
+ return CGRect(
414
+ x: rect.minX + (rect.width - fittedSize.width) / 2,
415
+ y: rect.minY + (rect.height - fittedSize.height) / 2,
416
+ width: fittedSize.width,
417
+ height: fittedSize.height
418
+ )
419
+ }
420
+ #endif
421
+
422
+ extension View {
423
+ @ViewBuilder
424
+ func getSidebarAdaptable(enabled: Bool) -> some View {
425
+ if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) {
426
+ if enabled {
427
+ #if compiler(>=6.0)
428
+ self.tabViewStyle(.sidebarAdaptable)
429
+ #else
430
+ self
431
+ #endif
432
+ } else {
433
+ self
434
+ }
435
+ } else {
436
+ self
437
+ }
438
+ }
439
+
440
+ @ViewBuilder
441
+ func tabBadge(_ data: String?) -> some View {
442
+ if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) {
443
+ if let data {
444
+ #if !os(tvOS)
445
+ self.badge(data)
446
+ #else
447
+ self
448
+ #endif
449
+ } else {
450
+ self
451
+ }
452
+ } else {
453
+ self
454
+ }
455
+ }
456
+
457
+ #if !os(macOS)
458
+ @ViewBuilder
459
+ func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View {
460
+ self
461
+ .onChange(of: props.barTintColor) { _ in
462
+ updateTabBarAppearance(props: props, tabBar: tabBar)
463
+ }
464
+ .onChange(of: props.scrollEdgeAppearance) { _ in
465
+ updateTabBarAppearance(props: props, tabBar: tabBar)
466
+ }
467
+ .onChange(of: props.translucent) { _ in
468
+ updateTabBarAppearance(props: props, tabBar: tabBar)
469
+ }
470
+ .onChange(of: props.inactiveTintColor) { _ in
471
+ updateTabBarAppearance(props: props, tabBar: tabBar)
472
+ updateTabBarItemImages(props: props, tabBar: tabBar)
473
+ }
474
+ .onChange(of: props.activeTintColor) { _ in
475
+ updateTabBarAppearance(props: props, tabBar: tabBar)
476
+ updateTabBarItemImages(props: props, tabBar: tabBar)
477
+ }
478
+ .onChange(of: props.selectedActiveTintColor) { newValue in
479
+ tabBar?.tintColor = newValue
480
+ }
481
+ .onChange(of: props.iconsRevision) { _ in
482
+ updateTabBarItemImages(props: props, tabBar: tabBar)
483
+ }
484
+ .onChange(of: props.labeled) { _ in
485
+ updateTabBarItemImages(props: props, tabBar: tabBar)
486
+ }
487
+ .onChange(of: props.fontSize) { _ in
488
+ updateTabBarAppearance(props: props, tabBar: tabBar)
489
+ updateTabBarItemImages(props: props, tabBar: tabBar)
490
+ }
491
+ .onChange(of: props.fontFamily) { _ in
492
+ updateTabBarAppearance(props: props, tabBar: tabBar)
493
+ updateTabBarItemImages(props: props, tabBar: tabBar)
494
+ }
495
+ .onChange(of: props.fontWeight) { _ in
496
+ updateTabBarAppearance(props: props, tabBar: tabBar)
497
+ updateTabBarItemImages(props: props, tabBar: tabBar)
498
+ }
499
+ .onChange(of: props.experimentalBakedTintColors) { _ in
500
+ updateTabBarAppearance(props: props, tabBar: tabBar)
501
+ updateTabBarItemImages(props: props, tabBar: tabBar)
502
+ }
503
+ .onChange(of: props.tabBarHidden) { newValue in
504
+ tabBar?.isHidden = newValue
505
+ }
506
+ }
507
+ #endif
508
+
509
+ @ViewBuilder
510
+ func tintColor(_ color: PlatformColor?) -> some View {
511
+ let color = color.map(Color.init)
512
+
513
+ if #available(iOS 16.0, tvOS 16.0, macOS 13.0, *) {
514
+ self.tint(color)
515
+ } else {
516
+ self.accentColor(color)
517
+ }
518
+ }
519
+
520
+ @ViewBuilder
521
+ func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View {
522
+ #if compiler(>=6.2)
523
+ if #available(iOS 26.0, macOS 26.0, tvOS 26.0, *) {
524
+ if let behavior {
525
+ self.tabBarMinimizeBehavior(behavior.convert())
526
+ } else {
527
+ self
528
+ }
529
+ } else {
530
+ self
531
+ }
532
+ #else
533
+ self
534
+ #endif
535
+ }
536
+
537
+ @ViewBuilder
538
+ func hideTabBar(_ flag: Bool) -> some View {
539
+ #if !os(macOS)
540
+ if flag {
541
+ if #available(iOS 16.0, tvOS 16.0, *) {
542
+ self.toolbar(.hidden, for: .tabBar)
543
+ } else {
544
+ // We fallback to isHidden on UITabBar
545
+ self
546
+ }
547
+ } else {
548
+ self
549
+ }
550
+ #else
551
+ self
552
+ #endif
553
+ }
554
+
555
+ // Allows TabView to use unfilled SFSymbols.
556
+ // By default they are always filled.
557
+ @ViewBuilder
558
+ func noneSymbolVariant() -> some View {
559
+ if #available(iOS 15.0, tvOS 15.0, macOS 13.0, *) {
560
+ self
561
+ .environment(\.symbolVariants, .none)
562
+ } else {
563
+ self
564
+ }
565
+ }
566
+ }