Skip to content

Commit 93dd70d

Browse files
committed
feature: Tab bar is always floating above the tab contents, insets the safe area
- Make it possible to hide the tab bar
1 parent 622dfef commit 93dd70d

15 files changed

+211
-367
lines changed

LICENSE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
SOFTWARE.
22+
23+
Modifications:
24+
Copyright (c) 2025 Nomasystems S.L.

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
// swift-tools-version: 5.7
1+
// swift-tools-version: 6.0
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "CustomTabView",
88
platforms: [
9-
.iOS(.v13),
10-
.macOS(.v10_15)
9+
.iOS(.v15),
10+
.macOS(.v12)
1111
],
1212
products: [
1313
// Products define the executables and libraries a package produces, and make them visible to other packages.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
# SwiftUI CustomTabView
66

7-
![Language](https://img.shields.io/badge/Swift-5-orange?logo=Swift&logoColor=white)
8-
![Platforms](https://img.shields.io/badge/Platforms-iOS%2013+,%20macOS%2010.15+-white?labelColor=gray&style=flat)
7+
![Language](https://img.shields.io/badge/Swift-6-orange?logo=Swift&logoColor=white)
8+
![Platforms](https://img.shields.io/badge/Platforms-iOS%2015+,%20macOS%2012+-white?labelColor=gray&style=flat)
99
![License](https://img.shields.io/badge/License-MIT-white?labelColor=blue&style=flat)
1010

1111
</div>

Sources/CustomTabView/CustomTabView.swift

Lines changed: 57 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import SwiftUI
3333
/// - tabs: An array of values conforming to `Hashable` that represent the tabs. The order of tabs in this array **must** be reflected in the TabBar view provided.
3434
/// - selection: The currently selected tab.
3535
/// - content: A closure that provides the content for each tab. The order and number of elements in the closure **must** match the `tabs` parameter.
36-
@available(iOS 13.0, macOS 10.15, *)
3736
public struct CustomTabView<SelectionValue: Hashable, TabBarView: View, Content: View>: View {
3837

3938
// Tabs
@@ -61,15 +60,15 @@ public struct CustomTabView<SelectionValue: Hashable, TabBarView: View, Content:
6160
public var body: some View {
6261
if #available(iOS 18.0, macOS 15.0, *) {
6362
Group(subviews: content) { subviews in
64-
_LayoutView<TabBarView, SelectionValue>(
63+
_TabBarLayoutView(
6564
tabBarView: tabBarView,
6665
selectedTabIndex: tabIndices[selection] ?? 0,
67-
children: subviews
66+
subviews: subviews
6867
)
6968
}
7069
} else {
7170
_VariadicView.Tree(
72-
_VariadicViewLayout<TabBarView, SelectionValue>(
71+
_VariadicViewLayout<TabBarView>(
7372
tabBarView: tabBarView,
7473
selectedTabIndex: tabIndices[selection] ?? 0
7574
)
@@ -80,253 +79,88 @@ public struct CustomTabView<SelectionValue: Hashable, TabBarView: View, Content:
8079
}
8180
}
8281

83-
private struct _VariadicViewLayout<TabBarView: View, SelectionValue: Hashable>: _VariadicView_UnaryViewRoot {
84-
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
85-
@Environment(\.tabBarPosition) private var tabBarPosition: TabBarPosition
86-
82+
private struct _VariadicViewLayout<TabBarView: View>: _VariadicView_UnaryViewRoot {
8783
let tabBarView: TabBarView
8884
let selectedTabIndex: Int
8985

90-
private func contentView(children: _VariadicView.Children) -> some View {
91-
#if canImport(UIKit)
92-
return UITabBarControllerRepresentable(
93-
selectedTabIndex: selectedTabIndex,
94-
controlledViews: children.map { UIHostingController(rootView: $0) }
95-
)
96-
#elseif canImport(AppKit)
97-
return NSTabViewControllerRepresentable(
98-
selectedTabIndex: selectedTabIndex,
99-
controlledViews: children.map { NSHostingController(rootView: $0) }
100-
)
101-
#endif
102-
}
103-
104-
private func topBarView(children: _VariadicView.Children) -> some View {
105-
VStack(spacing: 0) {
106-
tabBarView
107-
108-
contentView(children: children)
109-
}
110-
}
111-
112-
private func bottomBarView(children: _VariadicView.Children) -> some View {
113-
VStack(spacing: 0) {
114-
contentView(children: children)
115-
116-
tabBarView
117-
}
118-
}
119-
120-
#if canImport(UIKit)
121-
// Keyboard visibility
122-
@State private var isKeyboardVisible: Bool = false
123-
124-
private func bottomBarViewiOS13(children: _VariadicView.Children) -> some View {
125-
VStack(spacing: 0) {
126-
contentView(children: children)
127-
128-
if !isKeyboardVisible {
129-
tabBarView
130-
}
131-
}
132-
.onReceive(keyboardPublisher) { isKeyboardVisible in
133-
self.isKeyboardVisible = isKeyboardVisible
134-
}
135-
}
136-
#endif
137-
138-
private func leftBarView(children: _VariadicView.Children) -> some View {
139-
HStack(spacing: 0) {
140-
tabBarView
141-
142-
contentView(children: children)
143-
}
144-
}
145-
146-
private func rightBarView(children: _VariadicView.Children) -> some View {
147-
HStack(spacing: 0) {
148-
contentView(children: children)
149-
150-
tabBarView
151-
}
152-
}
153-
154-
private func floatingBarView(children: _VariadicView.Children, edge: Edge) -> some View {
155-
let alignment: Alignment
156-
switch edge {
157-
case .top:
158-
alignment = .top
159-
case .leading:
160-
alignment = .leading
161-
case .bottom:
162-
alignment = .bottom
163-
case .trailing:
164-
alignment = .trailing
165-
}
166-
167-
return ZStack(alignment: alignment, content: {
168-
contentView(children: children)
169-
170-
tabBarView
171-
})
172-
}
173-
17486
@ViewBuilder
17587
func body(children: _VariadicView.Children) -> some View {
176-
switch tabBarPosition {
177-
case .edge(let edge):
178-
switch edge {
179-
case .top:
180-
topBarView(children: children)
181-
case .leading:
182-
switch layoutDirection {
183-
case .leftToRight:
184-
leftBarView(children: children)
185-
case .rightToLeft:
186-
rightBarView(children: children)
187-
@unknown default:
188-
leftBarView(children: children)
189-
}
190-
case .bottom:
191-
#if canImport(UIKit)
192-
if #available(iOS 14, *) {
193-
bottomBarView(children: children)
194-
.ignoresSafeArea(.keyboard, edges: .bottom)
195-
} else {
196-
bottomBarViewiOS13(children: children)
197-
}
198-
#elseif canImport(AppKit)
199-
bottomBarView(children: children)
200-
#endif
201-
case .trailing:
202-
switch layoutDirection {
203-
case .leftToRight:
204-
rightBarView(children: children)
205-
case .rightToLeft:
206-
leftBarView(children: children)
207-
@unknown default:
208-
rightBarView(children: children)
209-
}
210-
}
211-
case .floating(let edge):
212-
floatingBarView(children: children, edge: edge)
213-
}
88+
_TabBarLayoutView(tabBarView: tabBarView, selectedTabIndex: selectedTabIndex, subviews: children)
21489
}
21590
}
21691

217-
#if canImport(UIKit)
218-
extension _VariadicViewLayout: KeyboardReadable {}
219-
#endif
220-
221-
@available(iOS 18.0, macOS 15.0, *)
222-
private struct _LayoutView<TabBarView: View, SelectionValue: Hashable>: View {
92+
struct _TabBarLayoutView<TabBarView: View, Subviews>: View where Subviews: RandomAccessCollection, Subviews.Element: View & Identifiable, Subviews.Index == Int {
22393
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
224-
@Environment(\.tabBarPosition) private var tabBarPosition: TabBarPosition
225-
94+
@Environment(\.tabBarEdge) private var tabBarEdge: Edge
95+
96+
@State private var tabBarVisibility: [Int: TabBarVisibility] = [:]
97+
@State private var tabBarHeight: CGFloat = 0
98+
22699
let tabBarView: TabBarView
227100
let selectedTabIndex: Int
228-
let children: SubviewsCollection
101+
let subviews: Subviews
102+
103+
var body: some View {
104+
GeometryReader { proxy in
105+
ZStack(alignment: tabBarAlignment, content: {
106+
contentView(
107+
subviews: subviews,
108+
additionalSafeAreaInsets: .init(),
109+
additionalSafeAreaInsetsTabBarVisible: additionalSafeAreaInsetsTabBarVisible
110+
)
111+
#if canImport(UIKit)
112+
.ignoresSafeArea(.all, edges: .vertical)
113+
#endif
114+
tabBarView
115+
.opacity(tabBarVisibility[selectedTabIndex] == .hidden ? 0 : 1)
116+
.onPreferenceChange(TabBarBoundsForSafeAreaKey.self) {
117+
if let anchor = $0 {
118+
tabBarHeight = proxy[anchor].height }
119+
}
120+
})
121+
}
122+
}
229123

230-
private func contentView(children: SubviewsCollection) -> some View {
124+
private func contentView(subviews: Subviews, additionalSafeAreaInsets: EdgeInsets, additionalSafeAreaInsetsTabBarVisible: EdgeInsets) -> some View {
231125
#if canImport(UIKit)
232126
return UITabBarControllerRepresentable(
233127
selectedTabIndex: selectedTabIndex,
234-
controlledViews: children.map { UIHostingController(rootView: $0) }
128+
tabBarVisibility: $tabBarVisibility,
129+
controlledViews: subviews,
130+
additionalSafeAreaInsets: additionalSafeAreaInsets,
131+
additionalSafeAreaInsetsTabBarVisible: additionalSafeAreaInsetsTabBarVisible
235132
)
236133
#elseif canImport(AppKit)
237134
return NSTabViewControllerRepresentable(
238135
selectedTabIndex: selectedTabIndex,
239-
controlledViews: children.map { NSHostingController(rootView: $0) }
136+
controlledViews: subviews.map { NSHostingController(rootView: $0) }
240137
)
241138
#endif
242139
}
243-
244-
private func topBarView(children: SubviewsCollection) -> some View {
245-
VStack(spacing: 0) {
246-
tabBarView
247-
248-
contentView(children: children)
249-
}
250-
}
251-
252-
private func bottomBarView(children: SubviewsCollection) -> some View {
253-
VStack(spacing: 0) {
254-
contentView(children: children)
255-
256-
tabBarView
257-
}
258-
}
259-
260-
private func leftBarView(children: SubviewsCollection) -> some View {
261-
HStack(spacing: 0) {
262-
tabBarView
263-
264-
contentView(children: children)
265-
}
266-
}
267-
268-
private func rightBarView(children: SubviewsCollection) -> some View {
269-
HStack(spacing: 0) {
270-
contentView(children: children)
271-
272-
tabBarView
273-
}
274-
}
275-
276-
private func floatingBarView(children: SubviewsCollection, edge: Edge) -> some View {
277-
let alignment: Alignment
278-
switch edge {
140+
141+
private var additionalSafeAreaInsetsTabBarVisible: EdgeInsets {
142+
switch tabBarEdge {
279143
case .top:
280-
alignment = .top
144+
.init(top: tabBarHeight, leading: 0, bottom: 0, trailing: 0)
281145
case .leading:
282-
alignment = .leading
146+
.init(top: 0, leading: tabBarHeight, bottom: 0, trailing: 0)
283147
case .bottom:
284-
alignment = .bottom
148+
.init(top: 0, leading: 0, bottom: tabBarHeight, trailing: 0)
285149
case .trailing:
286-
alignment = .trailing
150+
.init(top: 0, leading: 0, bottom: 0, trailing: tabBarHeight)
287151
}
288-
289-
return ZStack(alignment: alignment, content: {
290-
contentView(children: children)
291-
292-
tabBarView
293-
})
294152
}
295-
296-
var body: some View {
297-
switch tabBarPosition {
298-
case .edge(let edge):
299-
switch edge {
300-
case .top:
301-
topBarView(children: children)
302-
case .leading:
303-
switch layoutDirection {
304-
case .leftToRight:
305-
leftBarView(children: children)
306-
case .rightToLeft:
307-
rightBarView(children: children)
308-
@unknown default:
309-
leftBarView(children: children)
310-
}
311-
case .bottom:
312-
#if canImport(UIKit)
313-
bottomBarView(children: children)
314-
.ignoresSafeArea(.keyboard, edges: .bottom)
315-
#elseif canImport(AppKit)
316-
bottomBarView(children: children)
317-
#endif
318-
case .trailing:
319-
switch layoutDirection {
320-
case .leftToRight:
321-
rightBarView(children: children)
322-
case .rightToLeft:
323-
leftBarView(children: children)
324-
@unknown default:
325-
rightBarView(children: children)
326-
}
327-
}
328-
case .floating(let edge):
329-
floatingBarView(children: children, edge: edge)
153+
154+
private var tabBarAlignment: Alignment {
155+
switch tabBarEdge {
156+
case .top:
157+
.top
158+
case .leading:
159+
.leading
160+
case .bottom:
161+
.bottom
162+
case .trailing:
163+
.trailing
330164
}
331165
}
332166
}

0 commit comments

Comments
 (0)