@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.
- package/README.md +32 -0
- package/android/build.gradle +132 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
- package/android/src/main/java/com/rcttabview/LatestRequestTracker.kt +19 -0
- package/android/src/main/java/com/rcttabview/RCTTabView.kt +689 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +222 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +19 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewPrewarm.kt +40 -0
- package/android/src/main/java/com/rcttabview/RebuildBatch.kt +31 -0
- package/android/src/main/java/com/rcttabview/TabBarVisibility.kt +14 -0
- package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
- package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +25 -0
- package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +24 -0
- package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +32 -0
- package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +24 -0
- package/android/src/test/java/com/rcttabview/LatestRequestTrackerTest.kt +37 -0
- package/android/src/test/java/com/rcttabview/RebuildBatchTest.kt +44 -0
- package/android/src/test/java/com/rcttabview/TabBarVisibilityTest.kt +58 -0
- package/app.plugin.js +1 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +33 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +19 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
- package/ios/BottomAccessoryProvider.swift +27 -0
- package/ios/Extensions.swift +101 -0
- package/ios/PlatformAliases.swift +43 -0
- package/ios/RCTBottomAccessoryComponentView.h +17 -0
- package/ios/RCTBottomAccessoryComponentView.mm +76 -0
- package/ios/RCTTabViewComponentView.h +16 -0
- package/ios/RCTTabViewComponentView.mm +285 -0
- package/ios/RepresentableView.swift +55 -0
- package/ios/SVG/CoreSVG.h +19 -0
- package/ios/SVG/CoreSVG.mm +177 -0
- package/ios/SVG/SvgDecoder.h +10 -0
- package/ios/SVG/SvgDecoder.mm +38 -0
- package/ios/TabAppearModifier.swift +38 -0
- package/ios/TabBarFontSize.swift +88 -0
- package/ios/TabItem.swift +35 -0
- package/ios/TabItemEventModifier.swift +163 -0
- package/ios/TabView/AnyTabView.swift +7 -0
- package/ios/TabView/LegacyTabView.swift +70 -0
- package/ios/TabView/NewTabView.swift +138 -0
- package/ios/TabViewImpl.swift +566 -0
- package/ios/TabViewProps.swift +109 -0
- package/ios/TabViewProvider.swift +368 -0
- package/ios/react-native-bottom-tabs-Bridging-Header.h +4 -0
- package/lib/module/BottomAccessoryView.js +47 -0
- package/lib/module/BottomAccessoryView.js.map +1 -0
- package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
- package/lib/module/DelayedFreeze.js +31 -0
- package/lib/module/DelayedFreeze.js.map +1 -0
- package/lib/module/NativeSVGDecoder.js +5 -0
- package/lib/module/NativeSVGDecoder.js.map +1 -0
- package/lib/module/SceneMap.js +28 -0
- package/lib/module/SceneMap.js.map +1 -0
- package/lib/module/TabView.js +294 -0
- package/lib/module/TabView.js.map +1 -0
- package/lib/module/TabViewNativeComponent.ts +73 -0
- package/lib/module/expo.js +35 -0
- package/lib/module/expo.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
- package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
- package/lib/module/utils/useBottomTabBarHeight.js +12 -0
- package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts +7 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
- package/lib/typescript/src/BottomAccessoryView.js +24 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +15 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.js +4 -0
- package/lib/typescript/src/DelayedFreeze.d.ts +7 -0
- package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
- package/lib/typescript/src/DelayedFreeze.js +21 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts +5 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
- package/lib/typescript/src/NativeSVGDecoder.js +2 -0
- package/lib/typescript/src/SceneMap.d.ts +9 -0
- package/lib/typescript/src/SceneMap.d.ts.map +1 -0
- package/lib/typescript/src/SceneMap.js +15 -0
- package/lib/typescript/src/TabView.d.ts +219 -0
- package/lib/typescript/src/TabView.d.ts.map +1 -0
- package/lib/typescript/src/TabView.js +157 -0
- package/lib/typescript/src/TabViewNativeComponent.d.ts +59 -0
- package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TabViewNativeComponent.js +4 -0
- package/lib/typescript/src/expo.d.ts +6 -0
- package/lib/typescript/src/expo.d.ts.map +1 -0
- package/lib/typescript/src/expo.js +34 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/index.js +11 -0
- package/lib/typescript/src/types.d.ts +31 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/types.js +1 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +2 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.js +2 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +1 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.js +9 -0
- package/lib/typescript/tsconfig.tsbuildinfo +1 -0
- package/package.json +135 -0
- package/react-native-bottom-tabs.podspec +43 -0
- package/src/BottomAccessoryView.tsx +59 -0
- package/src/BottomAccessoryViewNativeComponent.ts +27 -0
- package/src/DelayedFreeze.tsx +31 -0
- package/src/NativeSVGDecoder.ts +5 -0
- package/src/SceneMap.tsx +34 -0
- package/src/TabView.tsx +558 -0
- package/src/TabViewNativeComponent.ts +73 -0
- package/src/expo.ts +58 -0
- package/src/index.tsx +23 -0
- package/src/types.ts +36 -0
- package/src/utils/BottomTabBarHeightContext.ts +5 -0
- 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
|
+
}
|