├── DynamicTabPages.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── adriensurugue.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── DynamicTabPages ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── DynamicTabPages.entitlements ├── DynamicTabPagesApp.swift ├── LinearInterpolation.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── TabPagesModel.swift └── TabPagesView.swift └── README.md /DynamicTabPages.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9F499E1D29DABA7D006F1EEF /* LinearInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F499E1C29DABA7D006F1EEF /* LinearInterpolation.swift */; }; 11 | 9F8B1B1E29DD6BF700BCA2B8 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 9F8B1B1D29DD6BF700BCA2B8 /* README.md */; }; 12 | 9FCC844E29D9C55E00176802 /* DynamicTabPagesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCC844D29D9C55E00176802 /* DynamicTabPagesApp.swift */; }; 13 | 9FCC845029D9C55E00176802 /* TabPagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCC844F29D9C55E00176802 /* TabPagesView.swift */; }; 14 | 9FCC845229D9C55F00176802 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FCC845129D9C55F00176802 /* Assets.xcassets */; }; 15 | 9FCC845629D9C55F00176802 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FCC845529D9C55F00176802 /* Preview Assets.xcassets */; }; 16 | 9FCC845D29D9C57700176802 /* TabPagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCC845C29D9C57700176802 /* TabPagesModel.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 9F499E1C29DABA7D006F1EEF /* LinearInterpolation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearInterpolation.swift; sourceTree = ""; }; 21 | 9F8B1B1D29DD6BF700BCA2B8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 22 | 9FCC844A29D9C55E00176802 /* DynamicTabPages.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicTabPages.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 9FCC844D29D9C55E00176802 /* DynamicTabPagesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTabPagesApp.swift; sourceTree = ""; }; 24 | 9FCC844F29D9C55E00176802 /* TabPagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPagesView.swift; sourceTree = ""; }; 25 | 9FCC845129D9C55F00176802 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 9FCC845329D9C55F00176802 /* DynamicTabPages.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DynamicTabPages.entitlements; sourceTree = ""; }; 27 | 9FCC845529D9C55F00176802 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 28 | 9FCC845C29D9C57700176802 /* TabPagesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabPagesModel.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 9FCC844729D9C55E00176802 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 9FCC844129D9C55E00176802 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 9F8B1B1D29DD6BF700BCA2B8 /* README.md */, 46 | 9FCC844C29D9C55E00176802 /* DynamicTabPages */, 47 | 9FCC844B29D9C55E00176802 /* Products */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | 9FCC844B29D9C55E00176802 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 9FCC844A29D9C55E00176802 /* DynamicTabPages.app */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | 9FCC844C29D9C55E00176802 /* DynamicTabPages */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 9FCC844D29D9C55E00176802 /* DynamicTabPagesApp.swift */, 63 | 9FCC844F29D9C55E00176802 /* TabPagesView.swift */, 64 | 9FCC845C29D9C57700176802 /* TabPagesModel.swift */, 65 | 9FCC845129D9C55F00176802 /* Assets.xcassets */, 66 | 9F499E1C29DABA7D006F1EEF /* LinearInterpolation.swift */, 67 | 9FCC845329D9C55F00176802 /* DynamicTabPages.entitlements */, 68 | 9FCC845429D9C55F00176802 /* Preview Content */, 69 | ); 70 | path = DynamicTabPages; 71 | sourceTree = ""; 72 | }; 73 | 9FCC845429D9C55F00176802 /* Preview Content */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 9FCC845529D9C55F00176802 /* Preview Assets.xcassets */, 77 | ); 78 | path = "Preview Content"; 79 | sourceTree = ""; 80 | }; 81 | /* End PBXGroup section */ 82 | 83 | /* Begin PBXNativeTarget section */ 84 | 9FCC844929D9C55E00176802 /* DynamicTabPages */ = { 85 | isa = PBXNativeTarget; 86 | buildConfigurationList = 9FCC845929D9C55F00176802 /* Build configuration list for PBXNativeTarget "DynamicTabPages" */; 87 | buildPhases = ( 88 | 9FCC844629D9C55E00176802 /* Sources */, 89 | 9FCC844729D9C55E00176802 /* Frameworks */, 90 | 9FCC844829D9C55E00176802 /* Resources */, 91 | ); 92 | buildRules = ( 93 | ); 94 | dependencies = ( 95 | ); 96 | name = DynamicTabPages; 97 | productName = DynamicTabPages; 98 | productReference = 9FCC844A29D9C55E00176802 /* DynamicTabPages.app */; 99 | productType = "com.apple.product-type.application"; 100 | }; 101 | /* End PBXNativeTarget section */ 102 | 103 | /* Begin PBXProject section */ 104 | 9FCC844229D9C55E00176802 /* Project object */ = { 105 | isa = PBXProject; 106 | attributes = { 107 | BuildIndependentTargetsInParallel = 1; 108 | LastSwiftUpdateCheck = 1430; 109 | LastUpgradeCheck = 1430; 110 | TargetAttributes = { 111 | 9FCC844929D9C55E00176802 = { 112 | CreatedOnToolsVersion = 14.3; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 9FCC844529D9C55E00176802 /* Build configuration list for PBXProject "DynamicTabPages" */; 117 | compatibilityVersion = "Xcode 14.0"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = 9FCC844129D9C55E00176802; 125 | productRefGroup = 9FCC844B29D9C55E00176802 /* Products */; 126 | projectDirPath = ""; 127 | projectRoot = ""; 128 | targets = ( 129 | 9FCC844929D9C55E00176802 /* DynamicTabPages */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXResourcesBuildPhase section */ 135 | 9FCC844829D9C55E00176802 /* Resources */ = { 136 | isa = PBXResourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | 9F8B1B1E29DD6BF700BCA2B8 /* README.md in Resources */, 140 | 9FCC845629D9C55F00176802 /* Preview Assets.xcassets in Resources */, 141 | 9FCC845229D9C55F00176802 /* Assets.xcassets in Resources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXResourcesBuildPhase section */ 146 | 147 | /* Begin PBXSourcesBuildPhase section */ 148 | 9FCC844629D9C55E00176802 /* Sources */ = { 149 | isa = PBXSourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | 9FCC845029D9C55E00176802 /* TabPagesView.swift in Sources */, 153 | 9FCC844E29D9C55E00176802 /* DynamicTabPagesApp.swift in Sources */, 154 | 9FCC845D29D9C57700176802 /* TabPagesModel.swift in Sources */, 155 | 9F499E1D29DABA7D006F1EEF /* LinearInterpolation.swift in Sources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXSourcesBuildPhase section */ 160 | 161 | /* Begin XCBuildConfiguration section */ 162 | 9FCC845729D9C55F00176802 /* Debug */ = { 163 | isa = XCBuildConfiguration; 164 | buildSettings = { 165 | ALWAYS_SEARCH_USER_PATHS = NO; 166 | CLANG_ANALYZER_NONNULL = YES; 167 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 168 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 169 | CLANG_ENABLE_MODULES = YES; 170 | CLANG_ENABLE_OBJC_ARC = YES; 171 | CLANG_ENABLE_OBJC_WEAK = YES; 172 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 173 | CLANG_WARN_BOOL_CONVERSION = YES; 174 | CLANG_WARN_COMMA = YES; 175 | CLANG_WARN_CONSTANT_CONVERSION = YES; 176 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 177 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 178 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 179 | CLANG_WARN_EMPTY_BODY = YES; 180 | CLANG_WARN_ENUM_CONVERSION = YES; 181 | CLANG_WARN_INFINITE_RECURSION = YES; 182 | CLANG_WARN_INT_CONVERSION = YES; 183 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 184 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 185 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 186 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 187 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 188 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 189 | CLANG_WARN_STRICT_PROTOTYPES = YES; 190 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 191 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 192 | CLANG_WARN_UNREACHABLE_CODE = YES; 193 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 194 | COPY_PHASE_STRIP = NO; 195 | DEBUG_INFORMATION_FORMAT = dwarf; 196 | ENABLE_STRICT_OBJC_MSGSEND = YES; 197 | ENABLE_TESTABILITY = YES; 198 | GCC_C_LANGUAGE_STANDARD = gnu11; 199 | GCC_DYNAMIC_NO_PIC = NO; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_OPTIMIZATION_LEVEL = 0; 202 | GCC_PREPROCESSOR_DEFINITIONS = ( 203 | "DEBUG=1", 204 | "$(inherited)", 205 | ); 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 213 | MTL_FAST_MATH = YES; 214 | ONLY_ACTIVE_ARCH = YES; 215 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 216 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 217 | }; 218 | name = Debug; 219 | }; 220 | 9FCC845829D9C55F00176802 /* Release */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | ALWAYS_SEARCH_USER_PATHS = NO; 224 | CLANG_ANALYZER_NONNULL = YES; 225 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 227 | CLANG_ENABLE_MODULES = YES; 228 | CLANG_ENABLE_OBJC_ARC = YES; 229 | CLANG_ENABLE_OBJC_WEAK = YES; 230 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 231 | CLANG_WARN_BOOL_CONVERSION = YES; 232 | CLANG_WARN_COMMA = YES; 233 | CLANG_WARN_CONSTANT_CONVERSION = YES; 234 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 235 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 236 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 237 | CLANG_WARN_EMPTY_BODY = YES; 238 | CLANG_WARN_ENUM_CONVERSION = YES; 239 | CLANG_WARN_INFINITE_RECURSION = YES; 240 | CLANG_WARN_INT_CONVERSION = YES; 241 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 242 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 243 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 245 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 246 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 247 | CLANG_WARN_STRICT_PROTOTYPES = YES; 248 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 249 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 250 | CLANG_WARN_UNREACHABLE_CODE = YES; 251 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 252 | COPY_PHASE_STRIP = NO; 253 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 254 | ENABLE_NS_ASSERTIONS = NO; 255 | ENABLE_STRICT_OBJC_MSGSEND = YES; 256 | GCC_C_LANGUAGE_STANDARD = gnu11; 257 | GCC_NO_COMMON_BLOCKS = YES; 258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 259 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 260 | GCC_WARN_UNDECLARED_SELECTOR = YES; 261 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 262 | GCC_WARN_UNUSED_FUNCTION = YES; 263 | GCC_WARN_UNUSED_VARIABLE = YES; 264 | MTL_ENABLE_DEBUG_INFO = NO; 265 | MTL_FAST_MATH = YES; 266 | SWIFT_COMPILATION_MODE = wholemodule; 267 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 268 | }; 269 | name = Release; 270 | }; 271 | 9FCC845A29D9C55F00176802 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 275 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 276 | CODE_SIGN_ENTITLEMENTS = DynamicTabPages/DynamicTabPages.entitlements; 277 | CODE_SIGN_STYLE = Automatic; 278 | CURRENT_PROJECT_VERSION = 1; 279 | DEVELOPMENT_ASSET_PATHS = "\"DynamicTabPages/Preview Content\""; 280 | DEVELOPMENT_TEAM = 286V6NN9R7; 281 | ENABLE_HARDENED_RUNTIME = YES; 282 | ENABLE_PREVIEWS = YES; 283 | GENERATE_INFOPLIST_FILE = YES; 284 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 285 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 286 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 287 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 288 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 289 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 290 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 291 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 292 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 293 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 294 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 295 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 296 | MACOSX_DEPLOYMENT_TARGET = 13.2; 297 | MARKETING_VERSION = 1.0; 298 | PRODUCT_BUNDLE_IDENTIFIER = com.DynamicTabPages; 299 | PRODUCT_NAME = "$(TARGET_NAME)"; 300 | SDKROOT = auto; 301 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 302 | SWIFT_EMIT_LOC_STRINGS = YES; 303 | SWIFT_VERSION = 5.0; 304 | TARGETED_DEVICE_FAMILY = "1,2"; 305 | }; 306 | name = Debug; 307 | }; 308 | 9FCC845B29D9C55F00176802 /* Release */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 312 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 313 | CODE_SIGN_ENTITLEMENTS = DynamicTabPages/DynamicTabPages.entitlements; 314 | CODE_SIGN_STYLE = Automatic; 315 | CURRENT_PROJECT_VERSION = 1; 316 | DEVELOPMENT_ASSET_PATHS = "\"DynamicTabPages/Preview Content\""; 317 | DEVELOPMENT_TEAM = 286V6NN9R7; 318 | ENABLE_HARDENED_RUNTIME = YES; 319 | ENABLE_PREVIEWS = YES; 320 | GENERATE_INFOPLIST_FILE = YES; 321 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 322 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 323 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 324 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 325 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 326 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 327 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 328 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 329 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 330 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 331 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 332 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 333 | MACOSX_DEPLOYMENT_TARGET = 13.2; 334 | MARKETING_VERSION = 1.0; 335 | PRODUCT_BUNDLE_IDENTIFIER = com.DynamicTabPages; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | SDKROOT = auto; 338 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 339 | SWIFT_EMIT_LOC_STRINGS = YES; 340 | SWIFT_VERSION = 5.0; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | }; 343 | name = Release; 344 | }; 345 | /* End XCBuildConfiguration section */ 346 | 347 | /* Begin XCConfigurationList section */ 348 | 9FCC844529D9C55E00176802 /* Build configuration list for PBXProject "DynamicTabPages" */ = { 349 | isa = XCConfigurationList; 350 | buildConfigurations = ( 351 | 9FCC845729D9C55F00176802 /* Debug */, 352 | 9FCC845829D9C55F00176802 /* Release */, 353 | ); 354 | defaultConfigurationIsVisible = 0; 355 | defaultConfigurationName = Release; 356 | }; 357 | 9FCC845929D9C55F00176802 /* Build configuration list for PBXNativeTarget "DynamicTabPages" */ = { 358 | isa = XCConfigurationList; 359 | buildConfigurations = ( 360 | 9FCC845A29D9C55F00176802 /* Debug */, 361 | 9FCC845B29D9C55F00176802 /* Release */, 362 | ); 363 | defaultConfigurationIsVisible = 0; 364 | defaultConfigurationName = Release; 365 | }; 366 | /* End XCConfigurationList section */ 367 | }; 368 | rootObject = 9FCC844229D9C55E00176802 /* Project object */; 369 | } 370 | -------------------------------------------------------------------------------- /DynamicTabPages.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DynamicTabPages.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DynamicTabPages.xcodeproj/xcuserdata/adriensurugue.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DynamicTabPages.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DynamicTabPages/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 | -------------------------------------------------------------------------------- /DynamicTabPages/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /DynamicTabPages/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DynamicTabPages/DynamicTabPages.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /DynamicTabPages/DynamicTabPagesApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicTabPagesApp.swift 3 | // DynamicTabPages 4 | // 5 | // Created by Adrien Surugue on 02/04/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct DynamicTabPagesApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | TabPagesView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DynamicTabPages/LinearInterpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearInterpolation.swift 3 | // DynamicTabIndicators 4 | // 5 | // Created by Balaji on 07/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class LinearInterpolation { 11 | private var length: Int 12 | private var inputRange: [CGFloat] 13 | private var outputRange: [CGFloat] 14 | 15 | init(inputRange: [CGFloat], outputRange: [CGFloat]) { 16 | 17 | assert(inputRange.count == outputRange.count) 18 | self.length = inputRange.count - 1 19 | self.inputRange = inputRange 20 | self.outputRange = outputRange 21 | } 22 | 23 | func calculate(for x: CGFloat) -> CGFloat { 24 | 25 | if x <= inputRange[0] { return outputRange[0] } 26 | 27 | for index in 1...length { 28 | let x1 = inputRange[index - 1] 29 | let x2 = inputRange[index] 30 | 31 | let y1 = outputRange[index - 1] 32 | let y2 = outputRange[index] 33 | 34 | /// Linear Interpolation Formula: y1 + ((y2-y1) / (x2-x1)) x (x-x1) 35 | if x <= inputRange[index] { 36 | let y = y1 + ((y2-y1) / (x2-x1)) * (x-x1) 37 | return y 38 | } 39 | } 40 | return outputRange[length] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /DynamicTabPages/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DynamicTabPages/TabPagesModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabPagesModel.swift 3 | // DynamicTabPages 4 | // 5 | // Created by Adrien Surugue on 01/04/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct TabPagesModel: Identifiable, Hashable{ 12 | var id = UUID().uuidString 13 | var name: String 14 | var icon: String 15 | var minX = 0.0 16 | var width = 0.0 17 | } 18 | 19 | var tabPages: [TabPagesModel] = [.init(name: "Principal", icon: "circle.grid.2x2"), 20 | .init(name: "Catégories", icon: "folder"), 21 | .init(name: "Favoris", icon: "heart.fill"), 22 | .init(name: "Ventes", icon: "doc.plaintext.fill") 23 | ] 24 | -------------------------------------------------------------------------------- /DynamicTabPages/TabPagesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // TabPagesView 4 | // 5 | // Created by Adrien Surugue on 02/04/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabPagesView: View { 11 | 12 | @State private var tabs = tabPages 13 | @State private var currentTab = tabPages.first! 14 | @State private var offset = 0.0 15 | @State private var index = 0 16 | @State private var indicatorWidth: CGFloat = 0 17 | @State private var indicatorPosition: CGFloat = 0 18 | @State private var scrollTarget: Int? 19 | 20 | var body: some View { 21 | ZStack{ 22 | GeometryReader{geometry in 23 | let screenSize = geometry.size 24 | VStack(spacing:5){ 25 | 26 | TabButtons(screenSize: screenSize) 27 | 28 | //MARK: - TabView 29 | TabView(selection:$currentTab){ 30 | 31 | //MARK: - View 1 32 | Color.red 33 | .ignoresSafeArea() 34 | .overlay( 35 | GeometryReader{ proxy in 36 | Color.clear 37 | .onChange(of: proxy.frame(in: .global), perform: { value in 38 | if currentTab.name == tabs[0].name{ 39 | index = 0 40 | offset = value.minX - (screenSize.width * CGFloat(index)) 41 | //print("DEBUG: Global Offset=\(offset)") 42 | } 43 | scrollTarget = index 44 | updateTabFrame(value.width) 45 | }) 46 | }) 47 | .tag(tabs[0]) 48 | 49 | //MARK: - View 2 50 | Color.blue 51 | .ignoresSafeArea() 52 | .overlay( 53 | GeometryReader{ proxy in 54 | Color.clear 55 | .onChange(of: proxy.frame(in: .global), perform: { value in 56 | if currentTab.name == tabs[1].name{ 57 | index = 1 58 | offset = value.minX - (screenSize.width * CGFloat(index)) 59 | //print("DEBUG: Global Offset=\(offset)") 60 | } 61 | scrollTarget = index 62 | updateTabFrame(value.width) 63 | }) 64 | }) 65 | .tag(tabs[1]) 66 | 67 | //MARK: - View 3 68 | Color.yellow 69 | .ignoresSafeArea() 70 | .overlay( 71 | GeometryReader{ proxy in 72 | Color.clear 73 | .onChange(of: proxy.frame(in: .global), perform: { value in 74 | if currentTab.name == tabs[2].name{ 75 | index = 2 76 | offset = value.minX - (screenSize.width * CGFloat(index)) 77 | //print("DEBUG: Global Offset=\(offset)") 78 | } 79 | scrollTarget = index 80 | updateTabFrame(value.width) 81 | }) 82 | } 83 | ) 84 | .tag(tabs[2]) 85 | 86 | //MARK: - View 4 87 | Color.green 88 | .ignoresSafeArea() 89 | .overlay( 90 | GeometryReader{ proxy in 91 | Color.clear 92 | .onChange(of: proxy.frame(in: .global), perform: { value in 93 | if currentTab.name == tabs[3].name{ 94 | index = 3 95 | offset = value.minX - (screenSize.width * CGFloat(index)) 96 | //print("DEBUG: Global Offset=\(offset)") 97 | } 98 | scrollTarget = index 99 | updateTabFrame(value.width) 100 | }) 101 | }) 102 | .tag(tabs[3]) 103 | } 104 | .tabViewStyle(.page(indexDisplayMode: .never)) 105 | .ignoresSafeArea() 106 | } 107 | .ignoresSafeArea(.all, edges: .bottom) 108 | .frame(width: screenSize.width, height: screenSize.height) 109 | } 110 | VStack{ 111 | Text("Index = \(index)") 112 | Text(String(offset)) 113 | } 114 | } 115 | } 116 | 117 | //MARK: - TabButtons 118 | @ViewBuilder 119 | func TabButtons(screenSize: CGSize) -> some View{ 120 | ScrollViewReader{ scrollView in 121 | ScrollView(.horizontal, showsIndicators: false){ 122 | HStack(spacing: 20){ 123 | ForEach($tabs){ $tab in 124 | HStack(spacing: 4){ 125 | Image(systemName: tab.icon) 126 | Text(tab.name) 127 | } 128 | .id(tab.id) 129 | .onTapGesture { 130 | withAnimation(.easeOut){ 131 | currentTab = tab 132 | if currentTab.name == tabs[0].name{ 133 | index = 0 134 | } else if currentTab.name == tabs[1].name{ 135 | index = 1 136 | } else if currentTab.name == tabs[2].name{ 137 | index = 2 138 | } else if currentTab.name == tabs[3].name{ 139 | index = 3 140 | } 141 | updateTabFrame(screenSize.width) 142 | } 143 | } 144 | .overlay( 145 | GeometryReader{ proxy in 146 | Color.clear 147 | .onAppear(){ 148 | tab.width = proxy.frame(in: .global).width 149 | tab.minX = proxy.frame(in: .global).minX 150 | updateTabFrame(screenSize.width) 151 | } 152 | } 153 | ) 154 | .id(tab.id) 155 | } 156 | } 157 | .padding(.horizontal) 158 | .overlay(alignment: .bottomLeading, content: { 159 | Capsule() 160 | .frame(width:indicatorWidth ,height: 4) 161 | .offset(x:indicatorPosition,y: 10) 162 | }) 163 | .padding(.top, 10) 164 | .padding(.bottom, 10) 165 | } 166 | .onChange(of: scrollTarget, perform: { _ in 167 | withAnimation(.linear){ 168 | if let index = scrollTarget{ 169 | scrollView.scrollTo(tabs[index].id, anchor: .center) 170 | } 171 | } 172 | }) 173 | } 174 | } 175 | 176 | //Calculate Tab indicator position & width 177 | func updateTabFrame(_ tabViewWidth: CGFloat) { 178 | 179 | //Make an array with all minX offset 180 | let inputRange = tabs.indices.compactMap { index -> CGFloat? in 181 | 182 | return CGFloat(index) * tabViewWidth 183 | } 184 | 185 | //Make an array with all tab buttons width 186 | let outputRangeForWidth = tabs.compactMap { tab -> CGFloat? in 187 | return tab.width 188 | } 189 | 190 | //Make an array with all tab buttons minX position 191 | let outputRangeForPosition = tabs.compactMap { tab -> CGFloat? in 192 | return tab.minX 193 | } 194 | 195 | let widthInterpolation = LinearInterpolation(inputRange: inputRange, outputRange: outputRangeForWidth) 196 | let positionInterpolation = LinearInterpolation(inputRange: inputRange, outputRange: outputRangeForPosition) 197 | 198 | indicatorWidth = widthInterpolation.calculate(for: -offset) 199 | indicatorPosition = positionInterpolation.calculate(for: -offset) 200 | //print("Indicator Width = \(indicatorWidth)") 201 | //print("Indicator Position = \(indicatorPosition)") 202 | } 203 | } 204 | 205 | struct TabPagesView_Previews: PreviewProvider { 206 | static var previews: some View { 207 | TabPagesView() 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Tab Pages 2 | 3 | This iOS application is an example implementation of dynamic tabs with SwiftUI. The UI includes a tab view at the top, with four tabs showcasing different colors. As the user scrolls horizontally through the tabs, a line indicator moves to track the currently selected tab.
4 | 5 | Buy Me A Coffee 6 | 7 | 8 | ![alt text](https://res.cloudinary.com/dhdgnx4mc/image/upload/v1680684757/media/GitHub/dfokhzxqfxl4ieynj93c.gif) 9 | 10 | ## Features 11 | 12 | - Display of multiple dynamic tab pages 13 | - Smooth horizontal scroll UI with SwiftUI 14 | - Automatic updating of indicator position based on currently selected tab page 15 | 16 | ## Installation 17 | 18 | 1. Clone this repository to your computer. 19 | 2. Open the project in Xcode. 20 | 3. Press the "Run" button to build and run the application on an iOS simulator or connected device. 21 | 22 | ## Usage 23 | 24 | When the application is running, the user can scroll horizontally through the tabs to switch between pages. The indicator above the tabs will automatically move to indicate the currently selected tab page.
25 | 26 | 27 | --------------------------------------------------------------------------------