11import Foundation
22import SwiftUI
33import React
4+ @_spi ( Advanced) import SwiftUIIntrospect
5+
46
57/**
68 Props that component accepts. SwiftUI view gets re-rendered when ObservableObject changes.
@@ -20,14 +22,14 @@ class TabViewProps: ObservableObject {
2022 @Published var ignoresTopSafeArea : Bool = true
2123 @Published var disablePageAnimations : Bool = false
2224 @Published var hapticFeedbackEnabled : Bool = true
23-
25+
2426 var selectedActiveTintColor : UIColor ? {
2527 if let selectedPage = selectedPage,
2628 let tabData = items. findByKey ( selectedPage) ,
2729 let activeTintColor = tabData. activeTintColor {
2830 return activeTintColor
2931 }
30-
32+
3133 return activeTintColor
3234 }
3335}
@@ -37,11 +39,11 @@ class TabViewProps: ObservableObject {
3739 */
3840struct RepresentableView : UIViewRepresentable {
3941 var view : UIView
40-
42+
4143 func makeUIView( context: Context ) -> UIView {
4244 return view
4345 }
44-
46+
4547 func updateUIView( _ uiView: UIView , context: Context ) { }
4648}
4749
@@ -52,7 +54,9 @@ struct TabViewImpl: View {
5254 @ObservedObject var props : TabViewProps
5355 var onSelect : ( _ key: String ) -> Void
5456 var onLongPress : ( _ key: String ) -> Void
55-
57+ @Weak var tabBar : UITabBar ?
58+
59+
5660 var body : some View {
5761 TabView ( selection: $props. selectedPage) {
5862 ForEach ( props. children. indices, id: \. self) { index in
@@ -63,7 +67,7 @@ struct TabViewImpl: View {
6367 guard let key = props. items. filter ( {
6468 !$0. hidden || $0. key == props. selectedPage
6569 } ) [ safe: index] ? . key else { return }
66-
70+
6771 if isLongPress {
6872 onLongPress ( key)
6973 emitHapticFeedback ( longPress: true )
@@ -72,9 +76,15 @@ struct TabViewImpl: View {
7276 emitHapticFeedback ( )
7377 }
7478 } )
79+ . introspectTabView ( closure: { tabController in
80+ tabBar = tabController. tabBar
81+ } )
82+ . onChange ( of: tabBar) { newValue in
83+ updateTabBarAppearance ( props: props, tabBar: tabBar)
84+ }
85+ . configureAppearance ( props: props, tabBar: tabBar)
7586 . tintColor ( props. selectedActiveTintColor)
7687 . getSidebarAdaptable ( enabled: props. sidebarAdaptable ?? false )
77- . configureAppearance ( props: props)
7888 . onChange ( of: props. selectedPage ?? " " ) { newValue in
7989 if ( props. disablePageAnimations) {
8090 UIView . setAnimationsEnabled ( false )
@@ -84,17 +94,17 @@ struct TabViewImpl: View {
8494 }
8595 }
8696 }
87-
97+
8898 @ViewBuilder
8999 private func renderTabItem( at index: Int ) -> some View {
90100 let tabData = props. items [ safe: index]
91101 let isHidden = tabData? . hidden ?? false
92102 let isFocused = props. selectedPage == tabData? . key
93-
103+
94104 if !isHidden || isFocused {
95105 let child = props. children [ safe: index] ?? UIView ( )
96106 let icon = props. icons [ index]
97-
107+
98108 RepresentableView ( view: child)
99109 . ignoresTopSafeArea (
100110 props. ignoresTopSafeArea,
@@ -120,13 +130,13 @@ struct TabViewImpl: View {
120130#endif
121131 }
122132 }
123-
133+
124134 func emitHapticFeedback( longPress: Bool = false ) {
125135#if os(iOS)
126136 if !props. hapticFeedbackEnabled {
127137 return
128138 }
129-
139+
130140 if longPress {
131141 UINotificationFeedbackGenerator ( ) . notificationOccurred ( . success)
132142 } else {
@@ -136,72 +146,12 @@ struct TabViewImpl: View {
136146 }
137147}
138148
139- @available ( iOS 15 . 0 , * )
140- private func configureAppearance( for appearanceType: String , appearance: UITabBarAppearance ) -> UITabBarAppearance {
141- if ( appearanceType == " transparent " ) {
142- return appearance
143- }
144-
145- switch appearanceType {
146- case " opaque " :
147- appearance. configureWithOpaqueBackground ( )
148- default :
149- appearance. configureWithDefaultBackground ( )
150- }
151-
152- UITabBar . appearance ( ) . scrollEdgeAppearance = appearance
153-
154- return appearance
155- }
156-
157- private func setTabBarItemColors( _ itemAppearance: UITabBarItemAppearance , inactiveColor: UIColor ) {
158- itemAppearance. normal. iconColor = inactiveColor
159- itemAppearance. normal. titleTextAttributes = [ . foregroundColor: inactiveColor]
160- }
161-
162- private func configureAppearance( inactiveTint inactiveTintColor: UIColor ? , appearance: UITabBarAppearance ) -> UITabBarAppearance {
163- // @see https://stackoverflow.com/a/71934882
164- if let inactiveTintColor {
165- setTabBarItemColors ( appearance. stackedLayoutAppearance, inactiveColor: inactiveTintColor)
166- setTabBarItemColors ( appearance. inlineLayoutAppearance, inactiveColor: inactiveTintColor)
167- setTabBarItemColors ( appearance. compactInlineLayoutAppearance, inactiveColor: inactiveTintColor)
168- }
169-
170- return appearance
171- }
172-
173- private func updateTabBarAppearance( props: TabViewProps ) {
174- var appearance = UITabBarAppearance ( )
175- appearance = configureAppearance (
176- inactiveTint: props. inactiveTintColor,
177- appearance: appearance
178- )
179-
180-
181- if #available( iOS 15 . 0 , * ) {
182- appearance = configureAppearance ( for: props. scrollEdgeAppearance ?? " " , appearance: appearance)
183-
184- if props. translucent == false {
185- appearance. configureWithOpaqueBackground ( )
186- }
187-
188- if props. barTintColor != nil {
189- appearance. backgroundColor = props. barTintColor
190- }
191- } else {
192- UITabBar . appearance ( ) . barTintColor = props. barTintColor
193- UITabBar . appearance ( ) . isTranslucent = props. translucent
194- }
195-
196- UITabBar . appearance ( ) . standardAppearance = appearance
197- }
198-
199149struct TabItem : View {
200150 var title : String ?
201151 var icon : UIImage ?
202152 var sfSymbol : String ?
203153 var labeled : Bool ?
204-
154+
205155 var body : some View {
206156 if let icon {
207157 Image ( uiImage: icon)
@@ -215,6 +165,40 @@ struct TabItem: View {
215165 }
216166}
217167
168+ private func updateTabBarAppearance( props: TabViewProps , tabBar: UITabBar ? ) {
169+ guard let tabBar else { return }
170+
171+ if ( props. scrollEdgeAppearance == " transparent " ) {
172+ tabBar. barTintColor = props. barTintColor
173+ tabBar. isTranslucent = props. translucent
174+ tabBar. unselectedItemTintColor = props. inactiveTintColor
175+
176+ return
177+ }
178+
179+ let appearance = UITabBarAppearance ( )
180+
181+
182+ appearance. configureWithDefaultBackground ( )
183+
184+ appearance. backgroundColor = props. barTintColor
185+
186+ if let inactiveTintColor = props. inactiveTintColor {
187+ let itemAppearance = UITabBarItemAppearance ( )
188+ itemAppearance. normal. iconColor = inactiveTintColor
189+ itemAppearance. normal. titleTextAttributes = [ . foregroundColor: inactiveTintColor]
190+
191+ appearance. stackedLayoutAppearance = itemAppearance
192+ appearance. inlineLayoutAppearance = itemAppearance
193+ appearance. compactInlineLayoutAppearance = itemAppearance
194+ }
195+
196+ tabBar. standardAppearance = appearance
197+ if #available( iOS 15 . 0 , * ) {
198+ tabBar. scrollEdgeAppearance = appearance. copy ( )
199+ }
200+ }
201+
218202extension View {
219203 @ViewBuilder
220204 func getSidebarAdaptable( enabled: Bool ) -> some View {
@@ -232,7 +216,7 @@ extension View {
232216 self
233217 }
234218 }
235-
219+
236220 @ViewBuilder
237221 func tabBadge( _ data: String ? ) -> some View {
238222 if #available( iOS 15 . 0 , macOS 15 . 0 , visionOS 2 . 0 , tvOS 15 . 0 , * ) {
@@ -249,7 +233,27 @@ extension View {
249233 self
250234 }
251235 }
252-
236+
237+ @ViewBuilder
238+ func configureAppearance( props: TabViewProps , tabBar: UITabBar ? ) -> some View {
239+ self
240+ . onChange ( of: props. barTintColor) { newValue in
241+ updateTabBarAppearance ( props: props, tabBar: tabBar)
242+ }
243+ . onChange ( of: props. scrollEdgeAppearance) { newValue in
244+ updateTabBarAppearance ( props: props, tabBar: tabBar)
245+ }
246+ . onChange ( of: props. translucent) { newValue in
247+ updateTabBarAppearance ( props: props, tabBar: tabBar)
248+ }
249+ . onChange ( of: props. inactiveTintColor) { newValue in
250+ updateTabBarAppearance ( props: props, tabBar: tabBar)
251+ }
252+ . onChange ( of: props. selectedActiveTintColor) { newValue in
253+ updateTabBarAppearance ( props: props, tabBar: tabBar)
254+ }
255+ }
256+
253257 @ViewBuilder
254258 func ignoresTopSafeArea(
255259 _ flag: Bool ,
@@ -266,30 +270,7 @@ extension View {
266270 . frame ( idealWidth: frame. width, idealHeight: frame. height)
267271 }
268272 }
269-
270- @ViewBuilder
271- func configureAppearance( props: TabViewProps ) -> some View {
272- self
273- . onAppear ( ) {
274- updateTabBarAppearance ( props: props)
275- }
276- . onChange ( of: props. barTintColor) { newValue in
277- updateTabBarAppearance ( props: props)
278- }
279- . onChange ( of: props. scrollEdgeAppearance) { newValue in
280- updateTabBarAppearance ( props: props)
281- }
282- . onChange ( of: props. translucent) { newValue in
283- updateTabBarAppearance ( props: props)
284- }
285- . onChange ( of: props. inactiveTintColor) { newValue in
286- updateTabBarAppearance ( props: props)
287- }
288- . onChange ( of: props. selectedActiveTintColor) { newValue in
289- updateTabBarAppearance ( props: props)
290- }
291- }
292-
273+
293274 @ViewBuilder
294275 func tintColor( _ color: UIColor ? ) -> some View {
295276 if let color {
@@ -303,7 +284,7 @@ extension View {
303284 self
304285 }
305286 }
306-
287+
307288 // Allows TabView to use unfilled SFSymbols.
308289 // By default they are always filled.
309290 @ViewBuilder
0 commit comments