Skip to content

Commit 05e3ea2

Browse files
committed
fix: appearance handling on iOS
1 parent 7b761e6 commit 05e3ea2

File tree

3 files changed

+96
-105
lines changed

3 files changed

+96
-105
lines changed

ios/Extensions.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,16 @@ extension UIImage {
3737
return resizedImage
3838
}
3939
}
40+
41+
extension View {
42+
@ViewBuilder
43+
func introspectTabView(closure: @escaping (UITabBarController) -> Void) -> some View {
44+
self
45+
.introspect(
46+
.tabView,
47+
on: .iOS(.v14, .v15, .v16, .v17, .v18),
48+
.tvOS(.v14,.v15, .v16, .v17, .v18),
49+
customize: closure
50+
)
51+
}
52+
}

ios/TabItemEventModifier.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ struct TabItemEventModifier: ViewModifier {
1818

1919
func body(content: Content) -> some View {
2020
content
21-
.introspect(.tabView, on: .iOS(.v14, .v15, .v16, .v17, .v18)) { tabController in
21+
.introspectTabView(closure: { tabController in
2222
handle(tabController: tabController)
23-
}
24-
.introspect(.tabView, on: .tvOS(.v14, .v15, .v16, .v17, .v18)) { tabController in
25-
handle(tabController: tabController)
26-
}
23+
})
2724
}
2825

2926
func handle(tabController: UITabBarController) {

ios/TabViewImpl.swift

Lines changed: 81 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Foundation
22
import SwiftUI
33
import 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
*/
3840
struct 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-
199149
struct 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+
218202
extension 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

Comments
 (0)