";
89 | };
90 | /* End PBXGroup section */
91 |
92 | /* Begin PBXNativeTarget section */
93 | 7831D2532A140BD300DC4BA1 /* Example */ = {
94 | isa = PBXNativeTarget;
95 | buildConfigurationList = 7831D2622A140BD300DC4BA1 /* Build configuration list for PBXNativeTarget "Example" */;
96 | buildPhases = (
97 | 7831D2502A140BD300DC4BA1 /* Sources */,
98 | 7831D2512A140BD300DC4BA1 /* Frameworks */,
99 | 7831D2522A140BD300DC4BA1 /* Resources */,
100 | );
101 | buildRules = (
102 | );
103 | dependencies = (
104 | );
105 | name = Example;
106 | packageProductDependencies = (
107 | 7800840B2A21F0B600F4E4B3 /* TabBarModule */,
108 | );
109 | productName = Example;
110 | productReference = 7831D2542A140BD300DC4BA1 /* Example.app */;
111 | productType = "com.apple.product-type.application";
112 | };
113 | /* End PBXNativeTarget section */
114 |
115 | /* Begin PBXProject section */
116 | 7831D24C2A140BD300DC4BA1 /* Project object */ = {
117 | isa = PBXProject;
118 | attributes = {
119 | BuildIndependentTargetsInParallel = 1;
120 | LastSwiftUpdateCheck = 1430;
121 | LastUpgradeCheck = 1430;
122 | TargetAttributes = {
123 | 7831D2532A140BD300DC4BA1 = {
124 | CreatedOnToolsVersion = 14.3;
125 | };
126 | };
127 | };
128 | buildConfigurationList = 7831D24F2A140BD300DC4BA1 /* Build configuration list for PBXProject "Example" */;
129 | compatibilityVersion = "Xcode 14.0";
130 | developmentRegion = en;
131 | hasScannedForEncodings = 0;
132 | knownRegions = (
133 | en,
134 | Base,
135 | );
136 | mainGroup = 7831D24B2A140BD300DC4BA1;
137 | productRefGroup = 7831D2552A140BD300DC4BA1 /* Products */;
138 | projectDirPath = "";
139 | projectRoot = "";
140 | targets = (
141 | 7831D2532A140BD300DC4BA1 /* Example */,
142 | );
143 | };
144 | /* End PBXProject section */
145 |
146 | /* Begin PBXResourcesBuildPhase section */
147 | 7831D2522A140BD300DC4BA1 /* Resources */ = {
148 | isa = PBXResourcesBuildPhase;
149 | buildActionMask = 2147483647;
150 | files = (
151 | 7831D25F2A140BD300DC4BA1 /* Preview Assets.xcassets in Resources */,
152 | 7831D25C2A140BD300DC4BA1 /* Assets.xcassets in Resources */,
153 | );
154 | runOnlyForDeploymentPostprocessing = 0;
155 | };
156 | /* End PBXResourcesBuildPhase section */
157 |
158 | /* Begin PBXSourcesBuildPhase section */
159 | 7831D2502A140BD300DC4BA1 /* Sources */ = {
160 | isa = PBXSourcesBuildPhase;
161 | buildActionMask = 2147483647;
162 | files = (
163 | 7831D25A2A140BD300DC4BA1 /* ContentView.swift in Sources */,
164 | 7831D2582A140BD300DC4BA1 /* ExampleApp.swift in Sources */,
165 | );
166 | runOnlyForDeploymentPostprocessing = 0;
167 | };
168 | /* End PBXSourcesBuildPhase section */
169 |
170 | /* Begin XCBuildConfiguration section */
171 | 7831D2602A140BD300DC4BA1 /* Debug */ = {
172 | isa = XCBuildConfiguration;
173 | buildSettings = {
174 | ALWAYS_SEARCH_USER_PATHS = NO;
175 | CLANG_ANALYZER_NONNULL = YES;
176 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
177 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
178 | CLANG_ENABLE_MODULES = YES;
179 | CLANG_ENABLE_OBJC_ARC = YES;
180 | CLANG_ENABLE_OBJC_WEAK = YES;
181 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
182 | CLANG_WARN_BOOL_CONVERSION = YES;
183 | CLANG_WARN_COMMA = YES;
184 | CLANG_WARN_CONSTANT_CONVERSION = YES;
185 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
186 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
187 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
188 | CLANG_WARN_EMPTY_BODY = YES;
189 | CLANG_WARN_ENUM_CONVERSION = YES;
190 | CLANG_WARN_INFINITE_RECURSION = YES;
191 | CLANG_WARN_INT_CONVERSION = YES;
192 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
193 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
194 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
195 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
196 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
197 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
198 | CLANG_WARN_STRICT_PROTOTYPES = YES;
199 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
200 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
201 | CLANG_WARN_UNREACHABLE_CODE = YES;
202 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
203 | COPY_PHASE_STRIP = NO;
204 | DEBUG_INFORMATION_FORMAT = dwarf;
205 | ENABLE_STRICT_OBJC_MSGSEND = YES;
206 | ENABLE_TESTABILITY = YES;
207 | GCC_C_LANGUAGE_STANDARD = gnu11;
208 | GCC_DYNAMIC_NO_PIC = NO;
209 | GCC_NO_COMMON_BLOCKS = YES;
210 | GCC_OPTIMIZATION_LEVEL = 0;
211 | GCC_PREPROCESSOR_DEFINITIONS = (
212 | "DEBUG=1",
213 | "$(inherited)",
214 | );
215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
217 | GCC_WARN_UNDECLARED_SELECTOR = YES;
218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
219 | GCC_WARN_UNUSED_FUNCTION = YES;
220 | GCC_WARN_UNUSED_VARIABLE = YES;
221 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
222 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
223 | MTL_FAST_MATH = YES;
224 | ONLY_ACTIVE_ARCH = YES;
225 | SDKROOT = iphoneos;
226 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
227 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
228 | };
229 | name = Debug;
230 | };
231 | 7831D2612A140BD300DC4BA1 /* Release */ = {
232 | isa = XCBuildConfiguration;
233 | buildSettings = {
234 | ALWAYS_SEARCH_USER_PATHS = NO;
235 | CLANG_ANALYZER_NONNULL = YES;
236 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
238 | CLANG_ENABLE_MODULES = YES;
239 | CLANG_ENABLE_OBJC_ARC = YES;
240 | CLANG_ENABLE_OBJC_WEAK = YES;
241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
242 | CLANG_WARN_BOOL_CONVERSION = YES;
243 | CLANG_WARN_COMMA = YES;
244 | CLANG_WARN_CONSTANT_CONVERSION = YES;
245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
248 | CLANG_WARN_EMPTY_BODY = YES;
249 | CLANG_WARN_ENUM_CONVERSION = YES;
250 | CLANG_WARN_INFINITE_RECURSION = YES;
251 | CLANG_WARN_INT_CONVERSION = YES;
252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
258 | CLANG_WARN_STRICT_PROTOTYPES = YES;
259 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
261 | CLANG_WARN_UNREACHABLE_CODE = YES;
262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
263 | COPY_PHASE_STRIP = NO;
264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
265 | ENABLE_NS_ASSERTIONS = NO;
266 | ENABLE_STRICT_OBJC_MSGSEND = YES;
267 | GCC_C_LANGUAGE_STANDARD = gnu11;
268 | GCC_NO_COMMON_BLOCKS = YES;
269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
271 | GCC_WARN_UNDECLARED_SELECTOR = YES;
272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
273 | GCC_WARN_UNUSED_FUNCTION = YES;
274 | GCC_WARN_UNUSED_VARIABLE = YES;
275 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
276 | MTL_ENABLE_DEBUG_INFO = NO;
277 | MTL_FAST_MATH = YES;
278 | SDKROOT = iphoneos;
279 | SWIFT_COMPILATION_MODE = wholemodule;
280 | SWIFT_OPTIMIZATION_LEVEL = "-O";
281 | VALIDATE_PRODUCT = YES;
282 | };
283 | name = Release;
284 | };
285 | 7831D2632A140BD300DC4BA1 /* Debug */ = {
286 | isa = XCBuildConfiguration;
287 | buildSettings = {
288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
290 | CODE_SIGN_STYLE = Automatic;
291 | CURRENT_PROJECT_VERSION = 1;
292 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
293 | DEVELOPMENT_TEAM = 2H684R3GX5;
294 | ENABLE_PREVIEWS = YES;
295 | GENERATE_INFOPLIST_FILE = YES;
296 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
297 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
298 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
299 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
300 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
301 | LD_RUNPATH_SEARCH_PATHS = (
302 | "$(inherited)",
303 | "@executable_path/Frameworks",
304 | );
305 | MARKETING_VERSION = 1.0;
306 | PRODUCT_BUNDLE_IDENTIFIER = com.zijievv.Example;
307 | PRODUCT_NAME = "$(TARGET_NAME)";
308 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
309 | SUPPORTS_MACCATALYST = NO;
310 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
311 | SWIFT_EMIT_LOC_STRINGS = YES;
312 | SWIFT_VERSION = 5.0;
313 | TARGETED_DEVICE_FAMILY = "1,2";
314 | };
315 | name = Debug;
316 | };
317 | 7831D2642A140BD300DC4BA1 /* Release */ = {
318 | isa = XCBuildConfiguration;
319 | buildSettings = {
320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
321 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
322 | CODE_SIGN_STYLE = Automatic;
323 | CURRENT_PROJECT_VERSION = 1;
324 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
325 | DEVELOPMENT_TEAM = 2H684R3GX5;
326 | ENABLE_PREVIEWS = YES;
327 | GENERATE_INFOPLIST_FILE = YES;
328 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
329 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
330 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
331 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
333 | LD_RUNPATH_SEARCH_PATHS = (
334 | "$(inherited)",
335 | "@executable_path/Frameworks",
336 | );
337 | MARKETING_VERSION = 1.0;
338 | PRODUCT_BUNDLE_IDENTIFIER = com.zijievv.Example;
339 | PRODUCT_NAME = "$(TARGET_NAME)";
340 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
341 | SUPPORTS_MACCATALYST = NO;
342 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
343 | SWIFT_EMIT_LOC_STRINGS = YES;
344 | SWIFT_VERSION = 5.0;
345 | TARGETED_DEVICE_FAMILY = "1,2";
346 | };
347 | name = Release;
348 | };
349 | /* End XCBuildConfiguration section */
350 |
351 | /* Begin XCConfigurationList section */
352 | 7831D24F2A140BD300DC4BA1 /* Build configuration list for PBXProject "Example" */ = {
353 | isa = XCConfigurationList;
354 | buildConfigurations = (
355 | 7831D2602A140BD300DC4BA1 /* Debug */,
356 | 7831D2612A140BD300DC4BA1 /* Release */,
357 | );
358 | defaultConfigurationIsVisible = 0;
359 | defaultConfigurationName = Release;
360 | };
361 | 7831D2622A140BD300DC4BA1 /* Build configuration list for PBXNativeTarget "Example" */ = {
362 | isa = XCConfigurationList;
363 | buildConfigurations = (
364 | 7831D2632A140BD300DC4BA1 /* Debug */,
365 | 7831D2642A140BD300DC4BA1 /* Release */,
366 | );
367 | defaultConfigurationIsVisible = 0;
368 | defaultConfigurationName = Release;
369 | };
370 | /* End XCConfigurationList section */
371 |
372 | /* Begin XCSwiftPackageProductDependency section */
373 | 7800840B2A21F0B600F4E4B3 /* TabBarModule */ = {
374 | isa = XCSwiftPackageProductDependency;
375 | productName = TabBarModule;
376 | };
377 | /* End XCSwiftPackageProductDependency section */
378 | };
379 | rootObject = 7831D24C2A140BD300DC4BA1 /* Project object */;
380 | }
381 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/BarOrange.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "filename" : "Orange.jpg",
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/BarOrange.imageset/Orange.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zijievv/swiftui-tab-bar/cd2b6b1ad81f5eb9985da9f154d61f1e411338ca/Example/Example/Assets.xcassets/BarOrange.imageset/Orange.jpg
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Example
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 | import TabBarModule
13 |
14 | struct ContentView: View {
15 | @Environment(\.colorScheme) private var colorScheme
16 | @State private var item: Int = 0
17 | @State private var visibility: Visibility = .automatic
18 | @State private var text: String = ""
19 |
20 | var body: some View {
21 | TabBar(selection: $item, visibility: $visibility) {
22 | homeView()
23 | .tabItem(0) {
24 | Image(systemName: item == 0 ? "house.fill" : "house")
25 | .font(.title3)
26 | Text("Home")
27 | .font(.system(.footnote, design: .rounded).weight(item == 0 ? .bold : .medium))
28 | } willSelect: {
29 | if item == 0 {
30 | text = ""
31 | }
32 | }
33 | marksView()
34 | .tabItem(1) {
35 | Image(systemName: item == 1 ? "star.fill" : "star")
36 | .font(.title3)
37 | Text("Marks")
38 | .font(.system(.footnote, design: .rounded).weight(item == 1 ? .bold : .medium))
39 | }
40 | userView()
41 | .tabItem(2) {
42 | Image(systemName: item == 2 ? "person.fill" : "person")
43 | .font(.title3)
44 | Text("User")
45 | .font(.system(.footnote, design: .rounded).weight(item == 2 ? .bold : .medium))
46 | }
47 | }
48 | .tabBarFill(.regularMaterial)
49 | .tabBarForeground {
50 | Image("BarOrange")
51 | .resizable()
52 | .scaledToFill()
53 | }
54 | .tabBarMargins(.vertical, 8)
55 | .tabBarPadding(.horizontal, 16)
56 | .tabBarPadding(.vertical, 8)
57 | .tabBarShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
58 | .tabBarShadow(color: .init(.sRGBLinear, white: colorScheme == .dark ? 1 : 0, opacity: 0.33), radius: 1, y: 2)
59 | .tabBarTransition(.move(edge: .bottom).combined(with: .opacity))
60 | .tabBarAnimation { isTabBarVisible in
61 | isTabBarVisible ? .easeInOut(duration: 0.2).delay(0.15) : .linear(duration: 0.25)
62 | }
63 | .tabBarItemsAlignment(.bottom)
64 | .overlay(alignment: .top) {
65 | Button("Visibility \(visibilityDescription)", action: nextVisibility)
66 | .buttonStyle(.borderedProminent)
67 | }
68 | }
69 |
70 | private func homeView() -> some View {
71 | TextField("", text: $text)
72 | .textFieldStyle(.roundedBorder)
73 | .padding()
74 | .background(.brown)
75 | }
76 |
77 | private func marksView() -> some View {
78 | List(1...30, id: \.self) {
79 | Text("Row \($0)")
80 | }
81 | }
82 |
83 | private func userView() -> some View {
84 | ZStack {
85 | Color.orange
86 | Text("User View")
87 | }
88 | }
89 |
90 | private func nextVisibility() {
91 | switch visibility {
92 | case .automatic: visibility = .visible
93 | case .visible: visibility = .hidden
94 | case .hidden: visibility = .automatic
95 | }
96 | }
97 |
98 | private var visibilityDescription: String {
99 | switch visibility {
100 | case .automatic: return "automatic"
101 | case .visible: return "visible"
102 | case .hidden: return "hidden"
103 | }
104 | }
105 | }
106 |
107 | struct ContentView_Previews: PreviewProvider {
108 | static var previews: some View {
109 | ContentView()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Example/Example/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleApp.swift
3 | // Example
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | @main
14 | struct ExampleApp: App {
15 | var body: some Scene {
16 | WindowGroup {
17 | ContentView()
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "swiftui-tab-bar",
8 | platforms: [
9 | .iOS(.v15)
10 | ],
11 | products: [
12 | .library(name: "TabBarModule", targets: ["TabBarModule"])
13 | ],
14 | targets: [
15 | .target(
16 | name: "TabBarModule",
17 | swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
18 | ),
19 | .testTarget(name: "TabBarTests", dependencies: ["TabBarModule"]),
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/Package@swift-5.5.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "swiftui-tab-bar",
8 | platforms: [
9 | .iOS(.v15)
10 | ],
11 | products: [
12 | .library(name: "TabBarModule", targets: ["TabBarModule"])
13 | ],
14 | targets: [
15 | .target(name: "TabBarModule"),
16 | .testTarget(name: "TabBarTests", dependencies: ["TabBarModule"]),
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | - [Usage](#usage)
3 | - [Shape and Fill Style](#shape-and-fill-style)
4 | - [Visibility with Animation and Transition](#visibility-with-animation-and-transition)
5 | - [Installation](#installation)
6 | - [Swift Package Manager (SPM)](#swift-package-manager-(spm))
7 | - [Xcode](#xcode)
8 |
9 | # TabBar
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | **`TabBar`** is a highly customizable tab bar view made in SwiftUI that functions similarly to [`TabView`](https://developer.apple.com/documentation/swiftui/tabview).
20 |
21 | ## Usage
22 |
23 | Similar to `TabView`, the `TabBar` accepts a Binding value that conforms to `Hashable`.
24 |
25 | ```swift
26 | import SwiftUI
27 | import TabBarModule
28 |
29 | struct ContentView: View {
30 | @State private var item: Int = 0
31 |
32 | var body: some View {
33 | TabBar(selection: $item) {
34 | HomeView()
35 | .tabItem(0) {
36 | Image(systemName: item == 0 ? "house.fill" : "house")
37 | .font(.title3)
38 | Text("Home")
39 | .font(.system(.footnote, design: .rounded).weight(item == 0 ? .bold : .medium))
40 | }
41 | MarksView()
42 | .tabItem(1) { /* ... */ }
43 | UserView()
44 | .tabItem(2) { /* ... */ }
45 | }
46 | }
47 | }
48 | ```
49 |
50 | The `TabBar` provides a default style when no other modifiers are set.
51 |
52 |
53 |
54 | With modifiers, it is easy to set the `TabBar`'s styles.
55 |
56 | ```swift
57 | TabBar(selection: $item) {
58 | // ...
59 | }
60 | .tabBarFill(.regularMaterial)
61 | .tabBarMargins(.vertical, 8)
62 | .tabBarPadding(.vertical, 8)
63 | .tabBarPadding(.horizontal, 16)
64 | .tabBarShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
65 | .tabBarShadow(radius: 1, y: 1)
66 | ```
67 |
68 |
69 |
70 | ### Shape and Fill Style
71 |
72 | The `TabBar` accepts any background shape that conforms to the `Shape` protocol (e.g., `Capsule`).
73 |
74 | ```swift
75 | TabBar(selection: $item) { /* ... */ }
76 | .tabBarPadding(.vertical, 8)
77 | .tabBarPadding(.horizontal, 16)
78 | .tabBarShape(Capsule(style: .continuous))
79 | .tabBarFill(.linearGradient(
80 | colors: [.yellow, .yellow.opacity(0.4)],
81 | startPoint: .top, endPoint: .bottom))
82 | ```
83 |
84 |
85 |
86 | The `TabBar` accepts any fill that conforms to the `ShapeStyle` protocol.
87 |
88 | ```swift
89 | TabBar(selection: $item) { /* ... */ }
90 | .tabBarFill(.linearGradient(
91 | colors: [.orange, .yellow], startPoint: .top, endPoint: .bottom))
92 | ```
93 |
94 |
95 |
96 | In addition to using `ShapeStyle` for filling, you can also use any view to set the foreground of the `TabBar`.
97 |
98 | ```swift
99 | TabBar(selection: $item) { /* ... */ }
100 | .tabBarForeground {
101 | Image("BarOrange").resizable().scaledToFill()
102 | }
103 | .tabBarShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
104 | .tabBarShadow(radius: 1, y: 2)
105 | ```
106 |
107 |
108 |
109 | ### Visibility with Animation and Transition
110 |
111 | The `TabBar` accepts a Binding value of type `Visibility` to control its visibility. When visibility is set to `.automatic`, the `TabBar` will observe the keyboard's appearance to automatically show or hide itself.
112 |
113 | You can customize the animation and transition for the appearance and disappearance of the `TabBar`.
114 |
115 | ```swift
116 | TabBar(selection: $item, visibility: $visibility) { /* ... */ }
117 | .tabBarTransition(.move(edge: .bottom).combined(with: .opacity))
118 | .tabBarAnimation { isTabBarVisible in
119 | isTabBarVisible ? .easeInOut : .linear
120 | }
121 | ```
122 |
123 | ## Installation
124 |
125 | Requirement: iOS 15.0+
126 |
127 | ### [Swift Package Manager](https://www.swift.org/package-manager/) (SPM)
128 |
129 | Add the following line to the dependencies in `Package.swift`, to use the `TabBarModule` in a SPM project:
130 |
131 | ```swift
132 | .package(url: "https://github.com/zijievv/swiftui-tab-bar", from: "0.0.1"),
133 | ```
134 |
135 | In your target:
136 |
137 | ```swift
138 | .target(name: "", dependencies: [
139 | .product(name: "TabBarModule", package: "swiftui-tab-bar"),
140 | // ...
141 | ]),
142 | ```
143 |
144 | Add `import TabBarModule` into your source code to use `TabBar`.
145 |
146 | ### Xcode
147 |
148 | Go to `File > Add Package Dependencies...` and paste the repo's URL:
149 |
150 | ```
151 | https://github.com/zijievv/swiftui-tab-bar.git
152 | ```
153 |
154 |
--------------------------------------------------------------------------------
/Resources/Images/CapsuleGradient-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zijievv/swiftui-tab-bar/cd2b6b1ad81f5eb9985da9f154d61f1e411338ca/Resources/Images/CapsuleGradient-half.png
--------------------------------------------------------------------------------
/Resources/Images/ForegroundView-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zijievv/swiftui-tab-bar/cd2b6b1ad81f5eb9985da9f154d61f1e411338ca/Resources/Images/ForegroundView-half.png
--------------------------------------------------------------------------------
/Resources/Images/RoundedRectShadow-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zijievv/swiftui-tab-bar/cd2b6b1ad81f5eb9985da9f154d61f1e411338ca/Resources/Images/RoundedRectShadow-half.png
--------------------------------------------------------------------------------
/Resources/Images/default-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zijievv/swiftui-tab-bar/cd2b6b1ad81f5eb9985da9f154d61f1e411338ca/Resources/Images/default-half.png
--------------------------------------------------------------------------------
/Resources/Images/defaultShapeGradient-half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zijievv/swiftui-tab-bar/cd2b6b1ad81f5eb9985da9f154d61f1e411338ca/Resources/Images/defaultShapeGradient-half.png
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarAnimationBuilderEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarAnimationBuilderEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 23.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarAnimationBuilderEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: (Bool) -> Animation? { { _ in .none } }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarAnimationBuilder: (Bool) -> Animation? {
19 | get { self[BarAnimationBuilderEnvironmentKey.self] }
20 | set { self[BarAnimationBuilderEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarFillStyleEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarFillStyleEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarFillStyleEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: FillStyle { .init() }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarFillStyle: FillStyle {
19 | get { self[BarFillStyleEnvironmentKey.self] }
20 | set { self[BarFillStyleEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarForegroundViewBuilderEnviromentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarForegroundViewBuilderEnviromentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 17.06.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarForegroundViewBuilderEnviromentKey: EnvironmentKey {
14 | static var defaultValue: (() -> AnyView)? { nil }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarForegroundViewBuilder: (() -> AnyView)? {
19 | get { self[BarForegroundViewBuilderEnviromentKey.self] }
20 | set { self[BarForegroundViewBuilderEnviromentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarItemsAlignmentEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarItemsAlignmentEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 09.06.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarItemsAlignmentEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: VerticalAlignment { .center }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarItemsAlignment: VerticalAlignment {
19 | get { self[BarItemsAlignmentEnvironmentKey.self] }
20 | set { self[BarItemsAlignmentEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarMarginsEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarMarginsEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarMarginsEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: EdgeInsets? { nil }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarMargins: EdgeInsets? {
19 | get { self[BarMarginsEnvironmentKey.self] }
20 | set { self[BarMarginsEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarPaddingEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarPaddingEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarPaddingEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: EdgeInsets? { nil }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarPadding: EdgeInsets? {
19 | get { self[BarPaddingEnvironmentKey.self] }
20 | set { self[BarPaddingEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarShadowEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarShadowEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarShadowEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: Shadow { .init(color: .clear, radius: 0, x: 0, y: 0) }
15 | }
16 |
17 | struct Shadow {
18 | let color: Color
19 | let radius: CGFloat
20 | let x: CGFloat
21 | let y: CGFloat
22 |
23 | init(color: Color, radius: CGFloat, x: CGFloat, y: CGFloat) {
24 | self.color = color
25 | self.radius = radius
26 | self.x = x
27 | self.y = y
28 | }
29 | }
30 |
31 | extension EnvironmentValues {
32 | var tabBarShadow: Shadow {
33 | get { self[BarShadowEnvironmentKey.self] }
34 | set { self[BarShadowEnvironmentKey.self] = newValue }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarShapeEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarShapeEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarShapeEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: (any Shape)? { nil }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarShape: (any Shape)? {
19 | get { self[BarShapeEnvironmentKey.self] }
20 | set { self[BarShapeEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarShapeStyleEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarShapeStyleEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarShapeStyleEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: AnyShapeStyle { .init(Material.bar) }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarShapeStyle: AnyShapeStyle {
19 | get { self[BarShapeStyleEnvironmentKey.self] }
20 | set { self[BarShapeStyleEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarSpacingEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarSpacingEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarSpacingEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: CGFloat? { nil }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarSpacing: CGFloat? {
19 | get { self[BarSpacingEnvironmentKey.self] }
20 | set { self[BarSpacingEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/BarTransitionEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BarTransitionEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 23.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct BarTransitionEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: AnyTransition { .identity }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabBarTransition: AnyTransition {
19 | get { self[BarTransitionEnvironmentKey.self] }
20 | set { self[BarTransitionEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/EnvironmentKey/ItemSelectionHashValueEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemSelectionHashValueEnvironmentKey.swift
3 | //
4 | //
5 | // Created by Zijie on 20.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct ItemSelectionHashValueEnvironmentKey: EnvironmentKey {
14 | static var defaultValue: Int? { nil }
15 | }
16 |
17 | extension EnvironmentValues {
18 | var tabItemSelectionHashValue: Int? {
19 | get { self[ItemSelectionHashValueEnvironmentKey.self] }
20 | set { self[ItemSelectionHashValueEnvironmentKey.self] = newValue }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/KeyboardObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardObserver.swift
3 | //
4 | //
5 | // Created by Zijie on 21.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | @MainActor
14 | class KeyboardObserver: ObservableObject {
15 | static let shared: KeyboardObserver = .init()
16 |
17 | @Published private(set) var keyboardWillShow = false
18 |
19 | private init() {
20 | NotificationCenter.default
21 | .addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { _ in
22 | Task { @MainActor in self.keyboardWillShow = true }
23 | }
24 |
25 | NotificationCenter.default
26 | .addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
27 | Task { @MainActor in self.keyboardWillShow = false }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/PreferenceKey/ItemActionWillSelectPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemActionWillSelectPreferenceKey.swift
3 | //
4 | //
5 | // Created by Zijie V on 24/08/2023.
6 | // Copyright © 2023 Zijie V . All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct ItemActionWillSelectPreferenceKey: PreferenceKey {
14 | static var defaultValue: [Selection: TabItemAction] { [:] }
15 |
16 | static func reduce(
17 | value: inout [Selection: TabItemAction],
18 | nextValue: () -> [Selection: TabItemAction]
19 | ) {
20 | value.merge(nextValue(), uniquingKeysWith: { $1 })
21 | }
22 | }
23 |
24 | struct TabItemAction: Hashable, Equatable {
25 | let selectedItemHashValue: Int?
26 | let item: Selection
27 | let actionWillSelect: ActionWillSelect?
28 |
29 | func hash(into hasher: inout Hasher) {
30 | hasher.combine(selectedItemHashValue)
31 | hasher.combine(item)
32 | }
33 |
34 | static func == (lhs: TabItemAction, rhs: TabItemAction) -> Bool {
35 | lhs.hashValue == rhs.hashValue
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/PreferenceKey/ItemViewBuilderPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemViewBuilderPreferenceKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct ItemViewBuilderPreferenceKey: PreferenceKey {
14 | static var defaultValue: [Selection: AnyItemViewBuilder] { [:] }
15 |
16 | static func reduce(
17 | value: inout [Selection: AnyItemViewBuilder],
18 | nextValue: () -> [Selection: AnyItemViewBuilder]
19 | ) {
20 | value.merge(nextValue(), uniquingKeysWith: { $1 })
21 | }
22 | }
23 |
24 | struct AnyItemViewBuilder: Hashable, Equatable {
25 | let selectedItemHashValue: Int?
26 | let item: Selection
27 | let content: () -> AnyView
28 |
29 | func hash(into hasher: inout Hasher) {
30 | hasher.combine(selectedItemHashValue)
31 | hasher.combine(item)
32 | }
33 |
34 | static func == (lhs: AnyItemViewBuilder, rhs: AnyItemViewBuilder) -> Bool {
35 | lhs.hashValue == rhs.hashValue
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/PreferenceKey/ItemsPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemsPreferenceKey.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct ItemsPreferenceKey: PreferenceKey {
14 | static var defaultValue: [Selection] { [] }
15 |
16 | static func reduce(value: inout [Selection], nextValue: () -> [Selection]) {
17 | value.append(contentsOf: nextValue())
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/ViewModifier/EdgeInsetsViewModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EdgeInsetsViewModifier.swift
3 | //
4 | //
5 | // Created by Zijie on 19.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct EdgeInsetsViewModifier: ViewModifier {
14 | private var envEdgeInsets: Environment
15 | private let path: WritableKeyPath
16 | private let edgeInsets: EdgeInsets
17 |
18 | init(keyPath: WritableKeyPath, edgeInsets: EdgeInsets) {
19 | self.envEdgeInsets = Environment(keyPath)
20 | self.path = keyPath
21 | self.edgeInsets = edgeInsets
22 | }
23 |
24 | func body(content: Content) -> some View {
25 | content.environment(path, newInsets())
26 | }
27 |
28 | private func newInsets() -> EdgeInsets {
29 | guard let old = envEdgeInsets.wrappedValue else { return edgeInsets }
30 | return .init(
31 | top: old.top + edgeInsets.top,
32 | leading: old.leading + edgeInsets.leading,
33 | bottom: old.bottom + edgeInsets.bottom,
34 | trailing: old.trailing + edgeInsets.trailing
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/ViewModifier/EdgeSetEdgeInsetsViewModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EdgeSetEdgeInsetsViewModifier.swift
3 | //
4 | //
5 | // Created by Zijie on 19.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct EdgeSetEdgeInsetsViewModifier: ViewModifier {
14 | private var envEdgeInsets: Environment
15 | private let path: WritableKeyPath
16 | private let edges: Edge.Set
17 | private let length: CGFloat
18 |
19 | init(keyPath: WritableKeyPath, edges: Edge.Set, length: CGFloat?) {
20 | self.envEdgeInsets = Environment(keyPath)
21 | self.path = keyPath
22 | self.edges = edges
23 | self.length = length ?? 8
24 | }
25 |
26 | func body(content: Content) -> some View {
27 | content.environment(path, new())
28 | }
29 |
30 | private func new() -> EdgeInsets {
31 | guard let old = envEdgeInsets.wrappedValue else {
32 | return .init(
33 | top: edges.contains(.top) ? length : 0,
34 | leading: edges.contains(.leading) ? length : 0,
35 | bottom: edges.contains(.bottom) ? length : 0,
36 | trailing: edges.contains(.trailing) ? length : 0
37 | )
38 | }
39 | return .init(
40 | top: inset(.top, with: old),
41 | leading: inset(.leading, with: old),
42 | bottom: inset(.bottom, with: old),
43 | trailing: inset(.trailing, with: old)
44 | )
45 | }
46 |
47 | private func inset(_ edge: Edge, with oldInsets: EdgeInsets) -> CGFloat {
48 | edges.contains(.init(edge)) ? length + oldInsets.length(of: edge) : oldInsets.length(of: edge)
49 | }
50 | }
51 |
52 | extension EdgeInsets {
53 | fileprivate func length(of edge: Edge) -> CGFloat {
54 | switch edge {
55 | case .top: return self.top
56 | case .leading: return self.leading
57 | case .bottom: return self.bottom
58 | case .trailing: return self.trailing
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/ViewModifier/InternalView+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InternalView+Extension.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | extension View {
14 | @ViewBuilder
15 | func `if`(_ predicate: @autoclosure () -> Bool, modifier: @escaping (Self) -> V) -> some View {
16 | if predicate() {
17 | modifier(self)
18 | } else {
19 | self
20 | }
21 | }
22 |
23 | func measurementSize(
24 | of path: KeyPath,
25 | to key: Key.Type
26 | ) -> some View where Key.Value == CGFloat {
27 | modifier(SizeMesurementViewModifier(path: path, key: key))
28 | }
29 |
30 | func foreground(_ content: @escaping () -> V) -> some View {
31 | self
32 | .foregroundStyle(.clear)
33 | .overlay {
34 | GeometryReader { geo in
35 | content()
36 | .frame(width: geo.size.width, height: geo.size.height)
37 | .clipped()
38 | .mask { self }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/ViewModifier/SizeMesurementViewModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SizeMesurementViewModifier.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct SizeMesurementViewModifier: ViewModifier where Key.Value == CGFloat {
14 | let path: KeyPath
15 | let key: Key.Type
16 |
17 | func body(content: Content) -> some View {
18 | content
19 | .background {
20 | GeometryReader { geo in
21 | Color.clear.preference(key: key, value: geo.size[keyPath: path])
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Internal/ViewModifier/TabItemViewModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabItemViewModifier.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | struct TabItemViewModifier: ViewModifier {
14 | @Environment(\.tabItemSelectionHashValue) private var selectionHashValue
15 | private let item: Selection
16 | @ViewBuilder private let itemBuilder: () -> V
17 | private let actionWillSelect: ActionWillSelect?
18 |
19 | init(item: Selection, @ViewBuilder itemBuilder: @escaping () -> V, willSelect action: ActionWillSelect?) {
20 | self.item = item
21 | self.itemBuilder = itemBuilder
22 | self.actionWillSelect = action
23 | }
24 |
25 | func body(content: Content) -> some View {
26 | content
27 | .opacity(selectionHashValue == item.hashValue ? 1 : 0)
28 | .disabled(!(selectionHashValue == item.hashValue))
29 | .preference(key: ItemsPreferenceKey.self, value: [item])
30 | .preference(
31 | key: ItemViewBuilderPreferenceKey.self,
32 | value: [
33 | item: AnyItemViewBuilder(
34 | selectedItemHashValue: selectionHashValue,
35 | item: item,
36 | content: { AnyView(VStack(spacing: 0, content: itemBuilder)) }
37 | )
38 | ]
39 | )
40 | .preference(
41 | key: ItemActionWillSelectPreferenceKey.self,
42 | value: [
43 | item: TabItemAction(
44 | selectedItemHashValue: selectionHashValue,
45 | item: item,
46 | actionWillSelect: actionWillSelect
47 | )
48 | ]
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Public/TabBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBar.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | public struct TabBar: View where Selection: Hashable, Content: View {
14 | @Environment(\.tabBarForegroundViewBuilder) private var barForegroundViewBuilder
15 | @Environment(\.tabBarAnimationBuilder) private var animationBuilder
16 | @Environment(\.tabBarItemsAlignment) private var itemsAlignment
17 | @Environment(\.tabBarTransition) private var barTransition
18 | @Environment(\.tabBarShapeStyle) private var shapeStyle
19 | @Environment(\.tabBarFillStyle) private var fillStyle
20 | @Environment(\.tabBarSpacing) private var barSpacing
21 | @Environment(\.tabBarMargins) private var barMargins
22 | @Environment(\.tabBarPadding) private var barPadding
23 | @Environment(\.tabBarShadow) private var barShadow
24 | @Environment(\.tabBarShape) private var barShape
25 | @State private var items: [Selection] = []
26 | @State private var tabItemBuilders: [Selection: AnyItemViewBuilder] = [:]
27 | @State private var tabItemActions: [Selection: TabItemAction] = [:]
28 | @Binding private var selection: Selection
29 | @Binding private var visibility: Visibility
30 | @StateObject private var keyboardObserver: KeyboardObserver = .shared
31 | private let content: () -> Content
32 |
33 | public init(
34 | selection: Binding,
35 | visibility: Binding = .constant(.automatic),
36 | @ViewBuilder content: @escaping () -> Content
37 | ) {
38 | self._selection = selection
39 | self._visibility = visibility
40 | self.content = content
41 | }
42 |
43 | public var body: some View {
44 | GeometryReader { geo in
45 | ZStack(content: content)
46 | .frame(maxWidth: .infinity, maxHeight: .infinity)
47 | .safeAreaInset(edge: .bottom, alignment: .center, spacing: barSpacing) { tabBar(in: geo) }
48 | .onPreferenceChange(ItemsPreferenceKey.self) { self.items = $0 }
49 | .onPreferenceChange(ItemViewBuilderPreferenceKey.self) { self.tabItemBuilders = $0 }
50 | .onPreferenceChange(ItemActionWillSelectPreferenceKey.self) { self.tabItemActions = $0 }
51 | .environment(\.tabItemSelectionHashValue, selection.hashValue)
52 | .animation(animationBuilder(isVisible), value: isVisible)
53 | }
54 | }
55 |
56 | private func tabBar(in geo: GeometryProxy) -> some View {
57 | Group {
58 | if isVisible {
59 | HStack(alignment: itemsAlignment, spacing: 0) {
60 | ForEach(items, id: \.hashValue) { tab(item: $0, width: itemWidth(in: geo.size.width)) }
61 | }
62 | .padding(margins)
63 | .background(alignment: .top) { GeometryReader(content: backgroundBar(with:)) }
64 | .padding(padding)
65 | .transition(barTransition)
66 | }
67 | }
68 | .measurementSize(of: \.height, to: TabBarHeightPreferenceKey.self)
69 | }
70 |
71 | @ViewBuilder
72 | private func tab(item: Selection, width: CGFloat) -> some View {
73 | if let builder = tabItemBuilders[item]?.content {
74 | builder()
75 | .contentShape(Rectangle())
76 | .onTapGesture {
77 | tabItemActions[item]?.actionWillSelect?()
78 | selection = item
79 | }
80 | .frame(width: width)
81 | }
82 | }
83 |
84 | private func backgroundBar(with geo: GeometryProxy) -> some View {
85 | filledViewBar(with: geo)
86 | .shadow(color: barShadow.color, radius: barShadow.radius, x: barShadow.x, y: barShadow.y)
87 | .ignoresSafeArea(edges: .horizontal)
88 | }
89 |
90 | @ViewBuilder
91 | private func filledViewBar(with geo: GeometryProxy) -> some View {
92 | if let barForegroundViewBuilder {
93 | filledShapStyleBar(with: geo)
94 | .foreground(barForegroundViewBuilder)
95 | } else {
96 | filledShapStyleBar(with: geo)
97 | }
98 | }
99 |
100 | private func filledShapStyleBar(with geo: GeometryProxy) -> some View {
101 | let filledShape = anyShapeBar.fill(shapeStyle, style: fillStyle)
102 | return AnyView(filledShape)
103 | .frame(height: isDefaultShape ? geo.size.height + geo.safeAreaInsets.bottom : geo.size.height)
104 | }
105 |
106 | private var anyShapeBar: any Shape { barShape ?? Rectangle() }
107 | private var isDefaultShape: Bool { barShape == nil }
108 | private var margins: EdgeInsets { barMargins ?? .init(top: 8, leading: 0, bottom: 8, trailing: 0) }
109 |
110 | private var padding: EdgeInsets {
111 | guard !isDefaultShape else { return .init(top: barPadding?.top ?? 0, leading: 0, bottom: 0, trailing: 0) }
112 | return barPadding ?? .init(top: 0, leading: 0, bottom: 0, trailing: 0)
113 | }
114 |
115 | private func itemWidth(in wholeWidth: CGFloat) -> CGFloat {
116 | (wholeWidth - padding.leading - padding.trailing - margins.leading - margins.trailing) / CGFloat(items.count)
117 | }
118 |
119 | private var isVisible: Bool {
120 | switch visibility {
121 | case .automatic:
122 | return !keyboardObserver.keyboardWillShow
123 | case .visible:
124 | return true
125 | case .hidden:
126 | return false
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Public/TabBarHeightPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarHeightPreferenceKey.swift
3 | //
4 | //
5 | // Created by Zijie on 20.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | /// Tab bar height preference key
14 | ///
15 | /// - Discussion:
16 | ///
17 | /// Until at least and including iOS 16, a content view within a `NavigationView` cannot access the safe area insets
18 | /// set by **`safeAreaInset(edge:alignment:spacing:content:)`** outside of the `NavigationView`. As a result, views
19 | /// within a `NavigationView` in `TabBar` are partially obscured by the tab bar. To resolve this issue, you can
20 | /// obtain the height of the tab bar outside of the `TabBar` using `TabBarHeightPreferenceKey`.
21 | ///
22 | /// ```swift
23 | /// struct ContentView: View {
24 | /// @State private var tabBarHeight: CGFloat = 0
25 | /// @State private var selection: Int = 0
26 | ///
27 | /// var body: some View {
28 | /// TabBar(selection: $selection) {
29 | /// NavigationView {
30 | /// HomeView()
31 | /// .safeAreaInset(edge: .bottom) { Color.clear.frame(height: tabBarHeight) }
32 | /// }
33 | /// .tabItem(0) { /* ... */ }
34 | ///
35 | /// NavigationView {
36 | /// AccountView()
37 | /// .safeAreaInset(edge: .bottom) { Color.clear.frame(height: tabBarHeight) }
38 | /// }
39 | /// .tabItem(1) { /* ... */ }
40 | /// }
41 | /// .onPreferenceChange(TabBarHeightPreferenceKey.self) { tabBarHeight = $0 }
42 | /// }
43 | /// }
44 | /// ```
45 | ///
46 | public struct TabBarHeightPreferenceKey: PreferenceKey {
47 | public static var defaultValue: CGFloat { .zero }
48 | public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
49 | value = max(value, nextValue())
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/TabBarModule/Public/View+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Extension.swift
3 | //
4 | //
5 | // Created by Zijie on 20.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import SwiftUI
12 |
13 | public typealias ActionWillSelect = () -> Void
14 |
15 | extension View {
16 | public func tabItem(
17 | _ selection: Selection,
18 | @ViewBuilder label: @escaping () -> V,
19 | willSelect action: ActionWillSelect? = nil
20 | ) -> some View {
21 | modifier(TabItemViewModifier(item: selection, itemBuilder: label, willSelect: action))
22 | }
23 |
24 | public func tabBarFill(_ content: S, style: FillStyle = .init()) -> some View {
25 | self.environment(\.tabBarShapeStyle, .init(AnyShapeStyle(content)))
26 | .environment(\.tabBarFillStyle, style)
27 | }
28 |
29 | public func tabBarForeground(_ content: @escaping () -> V) -> some View {
30 | environment(\.tabBarForegroundViewBuilder, { AnyView(content()) })
31 | }
32 |
33 | public func tabBarShape(_ shape: any Shape) -> some View {
34 | environment(\.tabBarShape, shape)
35 | }
36 |
37 | public func tabBarShadow(
38 | color: Color = .init(.sRGBLinear, white: 0, opacity: 0.33),
39 | radius: CGFloat,
40 | x: CGFloat = 0,
41 | y: CGFloat = 0
42 | ) -> some View {
43 | environment(\.tabBarShadow, .init(color: color, radius: radius, x: x, y: y))
44 | }
45 |
46 | public func tabBarAnimation(_ builder: @escaping (_ isTabBarVisible: Bool) -> Animation?) -> some View {
47 | environment(\.tabBarAnimationBuilder, builder)
48 | }
49 |
50 | public func tabBarTransition(_ t: AnyTransition) -> some View {
51 | environment(\.tabBarTransition, t)
52 | }
53 |
54 | /// Sets extra distance placed between the TabBar and the content view,
55 | /// or `nil` to use the default amount of spacing.
56 | public func tabBarSpacing(_ spacing: CGFloat? = nil) -> some View {
57 | environment(\.tabBarSpacing, spacing)
58 | }
59 |
60 | public func tabBarPadding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View {
61 | modifier(EdgeSetEdgeInsetsViewModifier(keyPath: \.tabBarPadding, edges: edges, length: length))
62 | }
63 |
64 | public func tabBarPadding(_ edgeInsets: EdgeInsets) -> some View {
65 | modifier(EdgeInsetsViewModifier(keyPath: \.tabBarPadding, edgeInsets: edgeInsets))
66 | }
67 |
68 | public func tabBarMargins(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View {
69 | modifier(EdgeSetEdgeInsetsViewModifier(keyPath: \.tabBarMargins, edges: edges, length: length))
70 | }
71 |
72 | public func tabBarMargins(_ edgeInsets: EdgeInsets) -> some View {
73 | modifier(EdgeInsetsViewModifier(keyPath: \.tabBarMargins, edgeInsets: edgeInsets))
74 | }
75 |
76 | public func tabBarItemsAlignment(_ alignment: VerticalAlignment) -> some View {
77 | environment(\.tabBarItemsAlignment, alignment)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/TabBarTests/TabBarTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarTests.swift
3 | //
4 | //
5 | // Created by Zijie on 18.05.2023.
6 | // Copyright © 2023 Zijie. All rights reserved.
7 | //
8 | // ====================================================================================================================
9 | //
10 |
11 | import XCTest
12 |
13 | @testable import TabBarModule
14 |
15 | final class TabBarTests: XCTestCase {
16 | func testExample() throws {
17 | // XCTest Documenation
18 | // https://developer.apple.com/documentation/xctest
19 |
20 | // Defining Test Cases and Test Methods
21 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
22 | }
23 | }
24 |
--------------------------------------------------------------------------------