├── .gitignore ├── .swift-format ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Example │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── BarOrange.imageset │ │ ├── Contents.json │ │ └── Orange.jpg │ └── Contents.json │ ├── ContentView.swift │ ├── ExampleApp.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── Package.swift ├── Package@swift-5.5.swift ├── README.md ├── Resources └── Images │ ├── CapsuleGradient-half.png │ ├── ForegroundView-half.png │ ├── RoundedRectShadow-half.png │ ├── default-half.png │ └── defaultShapeGradient-half.png ├── Sources └── TabBarModule │ ├── Internal │ ├── EnvironmentKey │ │ ├── BarAnimationBuilderEnvironmentKey.swift │ │ ├── BarFillStyleEnvironmentKey.swift │ │ ├── BarForegroundViewBuilderEnviromentKey.swift │ │ ├── BarItemsAlignmentEnvironmentKey.swift │ │ ├── BarMarginsEnvironmentKey.swift │ │ ├── BarPaddingEnvironmentKey.swift │ │ ├── BarShadowEnvironmentKey.swift │ │ ├── BarShapeEnvironmentKey.swift │ │ ├── BarShapeStyleEnvironmentKey.swift │ │ ├── BarSpacingEnvironmentKey.swift │ │ ├── BarTransitionEnvironmentKey.swift │ │ └── ItemSelectionHashValueEnvironmentKey.swift │ ├── KeyboardObserver.swift │ ├── PreferenceKey │ │ ├── ItemActionWillSelectPreferenceKey.swift │ │ ├── ItemViewBuilderPreferenceKey.swift │ │ └── ItemsPreferenceKey.swift │ └── ViewModifier │ │ ├── EdgeInsetsViewModifier.swift │ │ ├── EdgeSetEdgeInsetsViewModifier.swift │ │ ├── InternalView+Extension.swift │ │ ├── SizeMesurementViewModifier.swift │ │ └── TabItemViewModifier.swift │ └── Public │ ├── TabBar.swift │ ├── TabBarHeightPreferenceKey.swift │ └── View+Extension.swift └── Tests └── TabBarTests └── TabBarTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## macOS Env 9 | .DS_Store 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | /.build 54 | 55 | # CocoaPods 56 | # 57 | # We recommend against adding the Pods directory to your .gitignore. However 58 | # you should judge for yourself, the pros and cons are mentioned at: 59 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 60 | # 61 | # Pods/ 62 | # 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build/ 72 | 73 | # Accio dependency management 74 | Dependencies/ 75 | .accio/ 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "indentConditionalCompilationBlocks" : false, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineLength" : 120, 15 | "maximumBlankLines" : 1, 16 | "multiElementCollectionTrailingCommas" : true, 17 | "noAssignmentInExpressions" : { 18 | "allowedFunctions" : [ 19 | "XCTAssertNoThrow" 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether" : false, 23 | "respectsExistingLineBreaks" : true, 24 | "rules" : { 25 | "AllPublicDeclarationsHaveDocumentation" : false, 26 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 27 | "AlwaysUseLowerCamelCase" : true, 28 | "AmbiguousTrailingClosureOverload" : true, 29 | "BeginDocumentationCommentWithOneLineSummary" : false, 30 | "DoNotUseSemicolons" : true, 31 | "DontRepeatTypeInStaticProperties" : true, 32 | "FileScopedDeclarationPrivacy" : true, 33 | "FullyIndirectEnum" : true, 34 | "GroupNumericLiterals" : true, 35 | "IdentifiersMustBeASCII" : true, 36 | "NeverForceUnwrap" : false, 37 | "NeverUseForceTry" : false, 38 | "NeverUseImplicitlyUnwrappedOptionals" : false, 39 | "NoAccessLevelOnExtensionDeclaration" : true, 40 | "NoAssignmentInExpressions" : true, 41 | "NoBlockComments" : true, 42 | "NoCasesWithOnlyFallthrough" : true, 43 | "NoEmptyTrailingClosureParentheses" : true, 44 | "NoLabelsInCasePatterns" : true, 45 | "NoLeadingUnderscores" : false, 46 | "NoParensAroundConditions" : true, 47 | "NoPlaygroundLiterals" : true, 48 | "NoVoidReturnOnFunctionSignature" : true, 49 | "OmitExplicitReturns" : false, 50 | "OneCasePerLine" : true, 51 | "OneVariableDeclarationPerLine" : true, 52 | "OnlyOneTrailingClosureArgument" : false, 53 | "OrderedImports" : true, 54 | "ReplaceForEachWithForLoop" : false, 55 | "ReturnVoidInsteadOfEmptyTuple" : true, 56 | "TypeNamesShouldBeCapitalized" : true, 57 | "UseEarlyExits" : false, 58 | "UseLetInEveryBoundCaseVariable" : true, 59 | "UseShorthandTypeNames" : true, 60 | "UseSingleLinePropertyGetter" : true, 61 | "UseSynthesizedInitializer" : true, 62 | "UseTripleSlashForDocumentationComments" : true, 63 | "UseWhereClausesInForLoops" : false, 64 | "ValidateDocumentationComments" : false 65 | }, 66 | "spacesAroundRangeFormationOperators" : false, 67 | "tabWidth" : 8, 68 | "version" : 1 69 | } 70 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7800840C2A21F0B600F4E4B3 /* TabBarModule in Frameworks */ = {isa = PBXBuildFile; productRef = 7800840B2A21F0B600F4E4B3 /* TabBarModule */; }; 11 | 7831D2582A140BD300DC4BA1 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7831D2572A140BD300DC4BA1 /* ExampleApp.swift */; }; 12 | 7831D25A2A140BD300DC4BA1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7831D2592A140BD300DC4BA1 /* ContentView.swift */; }; 13 | 7831D25C2A140BD300DC4BA1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7831D25B2A140BD300DC4BA1 /* Assets.xcassets */; }; 14 | 7831D25F2A140BD300DC4BA1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7831D25E2A140BD300DC4BA1 /* Preview Assets.xcassets */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 7831D2542A140BD300DC4BA1 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 7831D2572A140BD300DC4BA1 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 20 | 7831D2592A140BD300DC4BA1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 21 | 7831D25B2A140BD300DC4BA1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | 7831D25E2A140BD300DC4BA1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 23 | 7831D2662A140D4100DC4BA1 /* TabBar */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TabBar; path = ..; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | 7831D2512A140BD300DC4BA1 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | 7800840C2A21F0B600F4E4B3 /* TabBarModule in Frameworks */, 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | 7831D24B2A140BD300DC4BA1 = { 39 | isa = PBXGroup; 40 | children = ( 41 | 7831D2562A140BD300DC4BA1 /* Example */, 42 | 7831D2652A140D4100DC4BA1 /* Packages */, 43 | 7831D2552A140BD300DC4BA1 /* Products */, 44 | 7831D2672A140E3D00DC4BA1 /* Frameworks */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 7831D2552A140BD300DC4BA1 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 7831D2542A140BD300DC4BA1 /* Example.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 7831D2562A140BD300DC4BA1 /* Example */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 7831D2572A140BD300DC4BA1 /* ExampleApp.swift */, 60 | 7831D2592A140BD300DC4BA1 /* ContentView.swift */, 61 | 7831D25B2A140BD300DC4BA1 /* Assets.xcassets */, 62 | 7831D25D2A140BD300DC4BA1 /* Preview Content */, 63 | ); 64 | path = Example; 65 | sourceTree = ""; 66 | }; 67 | 7831D25D2A140BD300DC4BA1 /* Preview Content */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 7831D25E2A140BD300DC4BA1 /* Preview Assets.xcassets */, 71 | ); 72 | path = "Preview Content"; 73 | sourceTree = ""; 74 | }; 75 | 7831D2652A140D4100DC4BA1 /* Packages */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 7831D2662A140D4100DC4BA1 /* TabBar */, 79 | ); 80 | name = Packages; 81 | sourceTree = ""; 82 | }; 83 | 7831D2672A140E3D00DC4BA1 /* Frameworks */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | ); 87 | name = Frameworks; 88 | sourceTree = ""; 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 | default-half 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 | RoundedRectShadow-half 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 | CapsuleGradient-half 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 | defaultShapeGradient-half 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 | ForegroundView-half 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 | --------------------------------------------------------------------------------