├── .gitignore ├── .spi.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Example └── AdaptiveTabExample │ ├── AdaptiveTabExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── AdaptiveTabExample │ ├── AdaptiveTabExampleApp.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── Sidebar │ └── SidebarView.swift │ └── Tabs │ ├── AppleWatchTabView.swift │ ├── FolderTabView.swift │ ├── MacOSTabView.swift │ └── iPhoneTabView.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Resources ├── iPad.png └── iPhone.png ├── Sources └── AdaptiveTabView │ ├── AdaptiveTabView.swift │ ├── Environment │ └── SelectedTabTransformer.swift │ ├── Extensions │ └── Either+SwiftUI.swift │ ├── Identifiers │ └── TabIdentifier.swift │ ├── PreviewContent │ └── PreviewTitleImageProvidingView.swift │ ├── Protocols │ └── TitleImageProviding.swift │ ├── SidebarLayout │ ├── SidebarItemNavigationLink.swift │ ├── SidebarLayoutView.swift │ └── SidebarView.swift │ ├── TabLayout │ ├── TabLayoutView.swift │ └── TabNavigationView.swift │ └── Typealiases │ └── TabContentView.swift └── Tests └── AdaptableTabViewTests └── AdaptableTabViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | *xcuserdata* -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [AdaptiveTabView] 5 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FF24DF4629BD1DA4009D1ECE /* AdaptiveTabExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF4529BD1DA4009D1ECE /* AdaptiveTabExampleApp.swift */; }; 11 | FF24DF4A29BD1DA5009D1ECE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FF24DF4929BD1DA5009D1ECE /* Assets.xcassets */; }; 12 | FF24DF4D29BD1DA5009D1ECE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FF24DF4C29BD1DA5009D1ECE /* Preview Assets.xcassets */; }; 13 | FF24DF5929BD5460009D1ECE /* AdaptiveTabView in Frameworks */ = {isa = PBXBuildFile; productRef = FF24DF5829BD5460009D1ECE /* AdaptiveTabView */; }; 14 | FF24DF5C29BD555D009D1ECE /* MacOSTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF5B29BD555D009D1ECE /* MacOSTabView.swift */; }; 15 | FF24DF5E29BD5789009D1ECE /* iPhoneTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF5D29BD5789009D1ECE /* iPhoneTabView.swift */; }; 16 | FF24DF6029BD582F009D1ECE /* AppleWatchTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF5F29BD582F009D1ECE /* AppleWatchTabView.swift */; }; 17 | FF3927002A01D6CF009B2657 /* FolderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3926FF2A01D6CF009B2657 /* FolderTabView.swift */; }; 18 | FF3F099529BEA92500866251 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F099429BEA92500866251 /* ContentView.swift */; }; 19 | FF3F099A29BFD3AC00866251 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F099929BFD3AC00866251 /* SidebarView.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | FF24DF4229BD1DA4009D1ECE /* AdaptiveTabExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdaptiveTabExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | FF24DF4529BD1DA4009D1ECE /* AdaptiveTabExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveTabExampleApp.swift; sourceTree = ""; }; 25 | FF24DF4929BD1DA5009D1ECE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | FF24DF4C29BD1DA5009D1ECE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | FF24DF5629BD1ED0009D1ECE /* AdaptiveTabView */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AdaptiveTabView; path = ../..; sourceTree = ""; }; 28 | FF24DF5B29BD555D009D1ECE /* MacOSTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacOSTabView.swift; sourceTree = ""; }; 29 | FF24DF5D29BD5789009D1ECE /* iPhoneTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPhoneTabView.swift; sourceTree = ""; }; 30 | FF24DF5F29BD582F009D1ECE /* AppleWatchTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchTabView.swift; sourceTree = ""; }; 31 | FF3926FF2A01D6CF009B2657 /* FolderTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderTabView.swift; sourceTree = ""; }; 32 | FF3F099429BEA92500866251 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 33 | FF3F099929BFD3AC00866251 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFrameworksBuildPhase section */ 37 | FF24DF3F29BD1DA4009D1ECE /* Frameworks */ = { 38 | isa = PBXFrameworksBuildPhase; 39 | buildActionMask = 2147483647; 40 | files = ( 41 | FF24DF5929BD5460009D1ECE /* AdaptiveTabView in Frameworks */, 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | FF24DF3929BD1DA4009D1ECE = { 49 | isa = PBXGroup; 50 | children = ( 51 | FF24DF5429BD1E9E009D1ECE /* Packages */, 52 | FF24DF4429BD1DA4009D1ECE /* AdaptiveTabExample */, 53 | FF24DF4329BD1DA4009D1ECE /* Products */, 54 | FF24DF5729BD5460009D1ECE /* Frameworks */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | FF24DF4329BD1DA4009D1ECE /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | FF24DF4229BD1DA4009D1ECE /* AdaptiveTabExample.app */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | FF24DF4429BD1DA4009D1ECE /* AdaptiveTabExample */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | FF24DF4529BD1DA4009D1ECE /* AdaptiveTabExampleApp.swift */, 70 | FF24DF5A29BD5552009D1ECE /* Tabs */, 71 | FF3F099629BFA04400866251 /* Sidebar */, 72 | FF3F099429BEA92500866251 /* ContentView.swift */, 73 | FF24DF4929BD1DA5009D1ECE /* Assets.xcassets */, 74 | FF24DF4B29BD1DA5009D1ECE /* Preview Content */, 75 | ); 76 | path = AdaptiveTabExample; 77 | sourceTree = ""; 78 | }; 79 | FF24DF4B29BD1DA5009D1ECE /* Preview Content */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | FF24DF4C29BD1DA5009D1ECE /* Preview Assets.xcassets */, 83 | ); 84 | path = "Preview Content"; 85 | sourceTree = ""; 86 | }; 87 | FF24DF5429BD1E9E009D1ECE /* Packages */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | FF24DF5629BD1ED0009D1ECE /* AdaptiveTabView */, 91 | ); 92 | name = Packages; 93 | sourceTree = ""; 94 | }; 95 | FF24DF5729BD5460009D1ECE /* Frameworks */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | ); 99 | name = Frameworks; 100 | sourceTree = ""; 101 | }; 102 | FF24DF5A29BD5552009D1ECE /* Tabs */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | FF24DF5B29BD555D009D1ECE /* MacOSTabView.swift */, 106 | FF24DF5D29BD5789009D1ECE /* iPhoneTabView.swift */, 107 | FF24DF5F29BD582F009D1ECE /* AppleWatchTabView.swift */, 108 | FF3926FF2A01D6CF009B2657 /* FolderTabView.swift */, 109 | ); 110 | path = Tabs; 111 | sourceTree = ""; 112 | }; 113 | FF3F099629BFA04400866251 /* Sidebar */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | FF3F099929BFD3AC00866251 /* SidebarView.swift */, 117 | ); 118 | path = Sidebar; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | FF24DF4129BD1DA4009D1ECE /* AdaptiveTabExample */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = FF24DF5029BD1DA5009D1ECE /* Build configuration list for PBXNativeTarget "AdaptiveTabExample" */; 127 | buildPhases = ( 128 | FF24DF3E29BD1DA4009D1ECE /* Sources */, 129 | FF24DF3F29BD1DA4009D1ECE /* Frameworks */, 130 | FF24DF4029BD1DA4009D1ECE /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = AdaptiveTabExample; 137 | packageProductDependencies = ( 138 | FF24DF5829BD5460009D1ECE /* AdaptiveTabView */, 139 | ); 140 | productName = AdaptiveTabExample; 141 | productReference = FF24DF4229BD1DA4009D1ECE /* AdaptiveTabExample.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | FF24DF3A29BD1DA4009D1ECE /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | BuildIndependentTargetsInParallel = 1; 151 | LastSwiftUpdateCheck = 1420; 152 | LastUpgradeCheck = 1420; 153 | TargetAttributes = { 154 | FF24DF4129BD1DA4009D1ECE = { 155 | CreatedOnToolsVersion = 14.2; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = FF24DF3D29BD1DA4009D1ECE /* Build configuration list for PBXProject "AdaptiveTabExample" */; 160 | compatibilityVersion = "Xcode 14.0"; 161 | developmentRegion = en; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | Base, 166 | ); 167 | mainGroup = FF24DF3929BD1DA4009D1ECE; 168 | productRefGroup = FF24DF4329BD1DA4009D1ECE /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | FF24DF4129BD1DA4009D1ECE /* AdaptiveTabExample */, 173 | ); 174 | }; 175 | /* End PBXProject section */ 176 | 177 | /* Begin PBXResourcesBuildPhase section */ 178 | FF24DF4029BD1DA4009D1ECE /* Resources */ = { 179 | isa = PBXResourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | FF24DF4D29BD1DA5009D1ECE /* Preview Assets.xcassets in Resources */, 183 | FF24DF4A29BD1DA5009D1ECE /* Assets.xcassets in Resources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXResourcesBuildPhase section */ 188 | 189 | /* Begin PBXSourcesBuildPhase section */ 190 | FF24DF3E29BD1DA4009D1ECE /* Sources */ = { 191 | isa = PBXSourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | FF24DF6029BD582F009D1ECE /* AppleWatchTabView.swift in Sources */, 195 | FF3F099529BEA92500866251 /* ContentView.swift in Sources */, 196 | FF24DF5C29BD555D009D1ECE /* MacOSTabView.swift in Sources */, 197 | FF24DF4629BD1DA4009D1ECE /* AdaptiveTabExampleApp.swift in Sources */, 198 | FF3927002A01D6CF009B2657 /* FolderTabView.swift in Sources */, 199 | FF24DF5E29BD5789009D1ECE /* iPhoneTabView.swift in Sources */, 200 | FF3F099A29BFD3AC00866251 /* SidebarView.swift in Sources */, 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | }; 204 | /* End PBXSourcesBuildPhase section */ 205 | 206 | /* Begin XCBuildConfiguration section */ 207 | FF24DF4E29BD1DA5009D1ECE /* Debug */ = { 208 | isa = XCBuildConfiguration; 209 | buildSettings = { 210 | ALWAYS_SEARCH_USER_PATHS = NO; 211 | CLANG_ANALYZER_NONNULL = YES; 212 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 213 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 214 | CLANG_ENABLE_MODULES = YES; 215 | CLANG_ENABLE_OBJC_ARC = YES; 216 | CLANG_ENABLE_OBJC_WEAK = YES; 217 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 218 | CLANG_WARN_BOOL_CONVERSION = YES; 219 | CLANG_WARN_COMMA = YES; 220 | CLANG_WARN_CONSTANT_CONVERSION = YES; 221 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 222 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 223 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 224 | CLANG_WARN_EMPTY_BODY = YES; 225 | CLANG_WARN_ENUM_CONVERSION = YES; 226 | CLANG_WARN_INFINITE_RECURSION = YES; 227 | CLANG_WARN_INT_CONVERSION = YES; 228 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 229 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 230 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 231 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 232 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 233 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 234 | CLANG_WARN_STRICT_PROTOTYPES = YES; 235 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 236 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 237 | CLANG_WARN_UNREACHABLE_CODE = YES; 238 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 239 | COPY_PHASE_STRIP = NO; 240 | DEBUG_INFORMATION_FORMAT = dwarf; 241 | ENABLE_STRICT_OBJC_MSGSEND = YES; 242 | ENABLE_TESTABILITY = YES; 243 | GCC_C_LANGUAGE_STANDARD = gnu11; 244 | GCC_DYNAMIC_NO_PIC = NO; 245 | GCC_NO_COMMON_BLOCKS = YES; 246 | GCC_OPTIMIZATION_LEVEL = 0; 247 | GCC_PREPROCESSOR_DEFINITIONS = ( 248 | "DEBUG=1", 249 | "$(inherited)", 250 | ); 251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 253 | GCC_WARN_UNDECLARED_SELECTOR = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 255 | GCC_WARN_UNUSED_FUNCTION = YES; 256 | GCC_WARN_UNUSED_VARIABLE = YES; 257 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 258 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 259 | MTL_FAST_MATH = YES; 260 | ONLY_ACTIVE_ARCH = YES; 261 | SDKROOT = iphoneos; 262 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 263 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 264 | }; 265 | name = Debug; 266 | }; 267 | FF24DF4F29BD1DA5009D1ECE /* Release */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ALWAYS_SEARCH_USER_PATHS = NO; 271 | CLANG_ANALYZER_NONNULL = YES; 272 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 273 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 274 | CLANG_ENABLE_MODULES = YES; 275 | CLANG_ENABLE_OBJC_ARC = YES; 276 | CLANG_ENABLE_OBJC_WEAK = YES; 277 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 278 | CLANG_WARN_BOOL_CONVERSION = YES; 279 | CLANG_WARN_COMMA = YES; 280 | CLANG_WARN_CONSTANT_CONVERSION = YES; 281 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 282 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 283 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 284 | CLANG_WARN_EMPTY_BODY = YES; 285 | CLANG_WARN_ENUM_CONVERSION = YES; 286 | CLANG_WARN_INFINITE_RECURSION = YES; 287 | CLANG_WARN_INT_CONVERSION = YES; 288 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 290 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 291 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 292 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 293 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 294 | CLANG_WARN_STRICT_PROTOTYPES = YES; 295 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 296 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 297 | CLANG_WARN_UNREACHABLE_CODE = YES; 298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 299 | COPY_PHASE_STRIP = NO; 300 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 301 | ENABLE_NS_ASSERTIONS = NO; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu11; 304 | GCC_NO_COMMON_BLOCKS = YES; 305 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 306 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 307 | GCC_WARN_UNDECLARED_SELECTOR = YES; 308 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 309 | GCC_WARN_UNUSED_FUNCTION = YES; 310 | GCC_WARN_UNUSED_VARIABLE = YES; 311 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 312 | MTL_ENABLE_DEBUG_INFO = NO; 313 | MTL_FAST_MATH = YES; 314 | SDKROOT = iphoneos; 315 | SWIFT_COMPILATION_MODE = wholemodule; 316 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 317 | VALIDATE_PRODUCT = YES; 318 | }; 319 | name = Release; 320 | }; 321 | FF24DF5129BD1DA5009D1ECE /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 325 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 326 | CODE_SIGN_STYLE = Automatic; 327 | CURRENT_PROJECT_VERSION = 1; 328 | DEVELOPMENT_ASSET_PATHS = "\"AdaptiveTabExample/Preview Content\""; 329 | DEVELOPMENT_TEAM = 538R2KUTXD; 330 | ENABLE_PREVIEWS = YES; 331 | GENERATE_INFOPLIST_FILE = YES; 332 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 333 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 334 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 335 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 336 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 337 | LD_RUNPATH_SEARCH_PATHS = ( 338 | "$(inherited)", 339 | "@executable_path/Frameworks", 340 | ); 341 | MARKETING_VERSION = 1.0; 342 | PRODUCT_BUNDLE_IDENTIFIER = com.mdfprojects.AdaptiveTabExample; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | SWIFT_EMIT_LOC_STRINGS = YES; 345 | SWIFT_VERSION = 5.0; 346 | TARGETED_DEVICE_FAMILY = "1,2"; 347 | }; 348 | name = Debug; 349 | }; 350 | FF24DF5229BD1DA5009D1ECE /* Release */ = { 351 | isa = XCBuildConfiguration; 352 | buildSettings = { 353 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 354 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 355 | CODE_SIGN_STYLE = Automatic; 356 | CURRENT_PROJECT_VERSION = 1; 357 | DEVELOPMENT_ASSET_PATHS = "\"AdaptiveTabExample/Preview Content\""; 358 | DEVELOPMENT_TEAM = 538R2KUTXD; 359 | ENABLE_PREVIEWS = YES; 360 | GENERATE_INFOPLIST_FILE = YES; 361 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 362 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 363 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 364 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 365 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 366 | LD_RUNPATH_SEARCH_PATHS = ( 367 | "$(inherited)", 368 | "@executable_path/Frameworks", 369 | ); 370 | MARKETING_VERSION = 1.0; 371 | PRODUCT_BUNDLE_IDENTIFIER = com.mdfprojects.AdaptiveTabExample; 372 | PRODUCT_NAME = "$(TARGET_NAME)"; 373 | SWIFT_EMIT_LOC_STRINGS = YES; 374 | SWIFT_VERSION = 5.0; 375 | TARGETED_DEVICE_FAMILY = "1,2"; 376 | }; 377 | name = Release; 378 | }; 379 | /* End XCBuildConfiguration section */ 380 | 381 | /* Begin XCConfigurationList section */ 382 | FF24DF3D29BD1DA4009D1ECE /* Build configuration list for PBXProject "AdaptiveTabExample" */ = { 383 | isa = XCConfigurationList; 384 | buildConfigurations = ( 385 | FF24DF4E29BD1DA5009D1ECE /* Debug */, 386 | FF24DF4F29BD1DA5009D1ECE /* Release */, 387 | ); 388 | defaultConfigurationIsVisible = 0; 389 | defaultConfigurationName = Release; 390 | }; 391 | FF24DF5029BD1DA5009D1ECE /* Build configuration list for PBXNativeTarget "AdaptiveTabExample" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | FF24DF5129BD1DA5009D1ECE /* Debug */, 395 | FF24DF5229BD1DA5009D1ECE /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | /* End XCConfigurationList section */ 401 | 402 | /* Begin XCSwiftPackageProductDependency section */ 403 | FF24DF5829BD5460009D1ECE /* AdaptiveTabView */ = { 404 | isa = XCSwiftPackageProductDependency; 405 | productName = AdaptiveTabView; 406 | }; 407 | /* End XCSwiftPackageProductDependency section */ 408 | }; 409 | rootObject = FF24DF3A29BD1DA4009D1ECE /* Project object */; 410 | } 411 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "sequencebuilder", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/andtie/SequenceBuilder.git", 7 | "state" : { 8 | "revision" : "54d3d1eff31a7e35122f616840fff11899ea85b4", 9 | "version" : "0.0.7" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/AdaptiveTabExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptiveTabExampleApp.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-03-11. 6 | // 7 | 8 | import SwiftUI 9 | import AdaptiveTabView 10 | 11 | @main 12 | struct AdaptiveTabExampleApp: App { 13 | @State private var selectedTab = iPhoneTabView.identifier 14 | @State private var columnVisibility: NavigationSplitViewVisibility = .doubleColumn 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | AdaptiveTabView( 19 | appName: "Example", 20 | selectedTab: $selectedTab, 21 | columnVisibility: $columnVisibility 22 | ) { (containerKind) in 23 | MacOSTabView() 24 | iPhoneTabView() 25 | AppleWatchTabView() 26 | if containerKind == .tabView { 27 | FolderTabView() 28 | } 29 | } defaultDetail: { 30 | ContentView(title: "Empty Details") 31 | } sidebarExtraContent: { 32 | SidebarView() 33 | } 34 | .selectedTabTransformer(transformer) 35 | .navigationSplitViewStyle(.automatic) 36 | } 37 | } 38 | 39 | let transformer = SelectedTabTransformer { (kind, tabIdentifier) in 40 | switch kind { 41 | case .tabView: 42 | let sharedTabViewIdentifiers = [ 43 | MacOSTabView.identifier, 44 | iPhoneTabView.identifier, 45 | AppleWatchTabView.identifier 46 | ] 47 | if !sharedTabViewIdentifiers.contains(tabIdentifier) { 48 | return FolderTabView.identifier 49 | } 50 | case .sidebarView: 51 | break 52 | } 53 | return tabIdentifier 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/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/AdaptiveTabExample/AdaptiveTabExample/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/AdaptiveTabExample/AdaptiveTabExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-03-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | let title: String 12 | 13 | var body: some View { 14 | Text(title) 15 | .navigationTitle(title) 16 | } 17 | } 18 | 19 | struct ContentView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | ContentView(title: "Hello World") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/Sidebar/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-03-13. 6 | // 7 | 8 | import SwiftUI 9 | import AdaptiveTabView 10 | 11 | struct SidebarView: View { 12 | let identifiers = [1, 2, 3, 4, 5] 13 | 14 | var body: some View { 15 | Group { 16 | Section("Folders") { 17 | ForEach(identifiers, id: \.self) { (identifier) in 18 | NavigationLink { 19 | ContentView(title: "Folder \(identifier)") 20 | } label: { 21 | Label("Folder \(identifier)", systemImage: "folder") 22 | } 23 | .tag(TabIdentifier("Folder\(identifier)")) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | struct SidebarView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | SidebarView() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/Tabs/AppleWatchTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppleWatchTabView.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-03-11. 6 | // 7 | 8 | import SwiftUI 9 | import AdaptiveTabView 10 | 11 | extension AppleWatchTabView { 12 | static let identifier = TabIdentifier("AppleWatchTabView") 13 | } 14 | 15 | struct AppleWatchTabView: View, TitleImageProviding { 16 | let title = "Apple Watches" 17 | let systemImageName = "applewatch" 18 | let id = AppleWatchTabView.identifier 19 | 20 | private let watches = [ 21 | "Apple Watch SE", 22 | "Apple Watch Ultra", 23 | "Apple Watch Series 8", 24 | "Apple Watch Series 7" 25 | ] 26 | 27 | var body: some View { 28 | List(watches, id: \.self) { (watch) in 29 | NavigationLink(watch) { 30 | ContentView(title: watch) 31 | } 32 | } 33 | .listStyle(.insetGrouped) 34 | } 35 | } 36 | 37 | struct AppleWatchTabView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | AppleWatchTabView() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/Tabs/FolderTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FolderTabView.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-05-02. 6 | // 7 | 8 | import SwiftUI 9 | import AdaptiveTabView 10 | 11 | extension FolderTabView { 12 | static let identifier = TabIdentifier("FolderTabView") 13 | } 14 | 15 | struct FolderTabView: View, TitleImageProviding { 16 | let title = "Folders" 17 | let systemImageName = "folder" 18 | let id = FolderTabView.identifier 19 | 20 | private let identifiers = [1, 2, 3, 4, 5] 21 | 22 | var body: some View { 23 | List { 24 | Section { 25 | ForEach(identifiers, id: \.self) { (identifier) in 26 | NavigationLink { 27 | ContentView(title: "Folder \(identifier)") 28 | } label: { 29 | Label("Folder \(identifier)", systemImage: "folder") 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | struct FolderTabView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | FolderTabView() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/Tabs/MacOSTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacOSTabView.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-03-11. 6 | // 7 | 8 | import SwiftUI 9 | import AdaptiveTabView 10 | 11 | extension MacOSTabView { 12 | static let identifier = TabIdentifier("MacOSTabView") 13 | } 14 | 15 | struct MacOSTabView: View, TitleImageProviding { 16 | let title = "macOS Versions" 17 | let systemImageName = "laptopcomputer" 18 | let id = MacOSTabView.identifier 19 | 20 | private let versions = [ 21 | "Ventura", 22 | "Monterey", 23 | "Big Sur", 24 | "Catalina", 25 | "Mojave" 26 | ] 27 | 28 | var body: some View { 29 | List(versions, id: \.self) { (version) in 30 | NavigationLink(version) { 31 | ContentView(title: version) 32 | } 33 | } 34 | .listStyle(.insetGrouped) 35 | } 36 | } 37 | 38 | struct FirstTabView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | MacOSTabView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/AdaptiveTabExample/AdaptiveTabExample/Tabs/iPhoneTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iPhoneTabView.swift 3 | // AdaptiveTabExample 4 | // 5 | // Created by Mark DiFranco on 2023-03-11. 6 | // 7 | 8 | import SwiftUI 9 | import AdaptiveTabView 10 | 11 | extension iPhoneTabView { 12 | static let identifier = TabIdentifier("iPhoneTabView") 13 | } 14 | 15 | struct iPhoneTabView: View, TitleImageProviding { 16 | let title = "iPhones" 17 | let systemImageName = "iphone" 18 | let id = iPhoneTabView.identifier 19 | 20 | private let phones = [ 21 | "iPhone 14 Pro Max", 22 | "iPhone 13 mini", 23 | "iPhone 12 Pro", 24 | "iPhone 11" 25 | ] 26 | 27 | var body: some View { 28 | List(phones, id: \.self) { (phone) in 29 | NavigationLink(phone) { 30 | ContentView(title: phone) 31 | } 32 | } 33 | .listStyle(.insetGrouped) 34 | } 35 | } 36 | 37 | struct iPhoneTabView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | iPhoneTabView() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mark DiFranco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "sequencebuilder", 5 | "kind" : "remoteSourceControl", 6 | "location" : "git@github.com:andtie/SequenceBuilder.git", 7 | "state" : { 8 | "revision" : "54d3d1eff31a7e35122f616840fff11899ea85b4", 9 | "version" : "0.0.7" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "AdaptiveTabView", 8 | platforms: [ 9 | .macOS(.v13), 10 | .iOS(.v16), 11 | .tvOS(.v16), 12 | ], 13 | products: [ 14 | .library( 15 | name: "AdaptiveTabView", 16 | targets: ["AdaptiveTabView"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/andtie/SequenceBuilder.git", from: "0.0.7") 20 | ], 21 | targets: [ 22 | .target( 23 | name: "AdaptiveTabView", 24 | dependencies: ["SequenceBuilder"]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdaptiveTabView 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmpdifran%2FAdaptiveTabView%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/mpdifran/AdaptiveTabView) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmpdifran%2FAdaptiveTabView%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/mpdifran/AdaptiveTabView) 5 | 6 | An adaptive SwiftUI container that switches between [TabView](https://developer.apple.com/design/human-interface-guidelines/components/navigation-and-search/tab-bars) and [NavigationSplitView](https://developer.apple.com/design/human-interface-guidelines/components/layout-and-organization/split-views) based on horiontal size class. This framework allows you to easily build iPhone and iPad apps that conform to [Apple's Human Interface Guidelines](https://developer.apple.com/design/). 7 | 8 | | iPhone | iPad | 9 | | ------ | ---- | 10 | |![Preview iPhone](https://github.com/mpdifran/AdaptiveTabView/blob/main/Resources/iPhone.png)|![Preview iPad](https://github.com/mpdifran/AdaptiveTabView/blob/main/Resources/iPad.png)| 11 | 12 | Here's an example of how it can be used: 13 | 14 | ```swift 15 | @main 16 | struct MyApp: App { 17 | @State private var selectedTab = MyFirstTab.identifier 18 | @State private var columnVisibility: NavigationSplitViewVisibility = .doubleColumn 19 | 20 | var body: some Scene { 21 | WindowGroup { 22 | AdaptiveTabView( 23 | appName: "My App", 24 | selectedTab: selectedTab 25 | ) { 26 | MyFirstTab() 27 | MySecondTab() 28 | MyThirdTab() 29 | } defaultContent: { 30 | MyDefaultContentView() 31 | } defaultDetail: { 32 | MyDefaultDetailView() 33 | } sidebarExtraContent: { 34 | Section { 35 | ForEach(folders) { (folder) in 36 | FolderSidebarCell(folder) 37 | } 38 | } 39 | } 40 | .selectedTabTransformer(transformer) 41 | } 42 | } 43 | 44 | let transformer = SelectedTabTransformer { (kind, tabIdentifier) in 45 | switch kind { 46 | case .tabView: 47 | let sharedTabViewIdentifiers = [ 48 | MyFirstTab.identifier, 49 | MySecondTab.identifier, 50 | MyThirdTab.identifier 51 | ] 52 | if !sharedTabViewIdentifiers.contains(tabIdentifier) { 53 | return MyFirstTab.identifier 54 | } 55 | case .sidebarView: 56 | break 57 | } 58 | return tabIdentifier 59 | } 60 | } 61 | ``` 62 | 63 | ```swift 64 | extension MyFirstTab { 65 | static let identifier = TabIdentifier("MyFirstTab") 66 | } 67 | 68 | struct MyFirstTab: View, TitleImageProviding { 69 | let title = "My First Tab" 70 | let systemImageName = "1.square" 71 | let id = MyFirstTab.identifier 72 | 73 | var body: some View { 74 | ... 75 | } 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /Resources/iPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpdifran/AdaptiveTabView/29db79831c4e2bbeeb50d56f9681a6be1bd8a5ad/Resources/iPad.png -------------------------------------------------------------------------------- /Resources/iPhone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpdifran/AdaptiveTabView/29db79831c4e2bbeeb50d56f9681a6be1bd8a5ad/Resources/iPhone.png -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/AdaptiveTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptiveTabView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | import SequenceBuilder 10 | 11 | // MARK: - Enums 12 | 13 | /// The type of container currently being used by the ``AdaptiveTabView``. 14 | public enum AdaptiveTabViewContainerKind { 15 | /// The content is being displayed in a ``TabView``. 16 | case tabView 17 | 18 | /// The content is being displayed in a ``NavigationSplitView`` with a sidebar. 19 | case sidebarView 20 | } 21 | 22 | /// The kind of split view to use when in ``AdaptiveTabViewContainerKind.sidebarView``. 23 | public enum AdaptiveTabViewSplitViewKind { 24 | /// A split view with only 2 columns. 25 | case twoColumn 26 | 27 | /// A split view with 3 columns 28 | case threeColumn 29 | } 30 | 31 | // MARK: - AdaptiveTabView 32 | 33 | /// A container that displays as a ``TabView`` when the horiontal size class is `.compact`, and displays a ``NavigationSplitView`` when 34 | /// it's `.regular`. This allows for simple support of iPhone and iPad screens in one component. 35 | public struct AdaptiveTabView: View where TabContent.Element: TabContentView { 36 | 37 | private let appName: String 38 | private let splitViewKind: AdaptiveTabViewSplitViewKind 39 | private let tabViewBuilder: (AdaptiveTabViewContainerKind) -> TabContent 40 | private let defaultContentBuilder: () -> DefaultContentView 41 | private let defaultDetailBuilder: () -> DefaultDetailView 42 | private let sidebarExtraContentBuilder: () -> SidebarExtraContent 43 | 44 | @Binding private var selectedTab: TabIdentifier 45 | @State private var selectedTabViewTab: TabIdentifier 46 | @State private var selectedSidebarViewTab: TabIdentifier 47 | 48 | private let columnVisibilityBinding: Binding? 49 | @State private var columnVisibilityState: NavigationSplitViewVisibility = .doubleColumn 50 | 51 | @Environment(\.selectedTabTransformer) var selectedTabTransformer 52 | 53 | /// Creates an ``AdaptiveTabView``. 54 | /// - parameter appName: The name of the app. This appears as the navigation title of the sidebar when the kind 55 | /// is ``AdaptiveTabViewContainerKind.sidebarView``. 56 | /// - parameter selectedTab: The identifier of the selected tab in the currently dsisplayed mode. 57 | /// - parameter splitViewKind: The type of split view to use. 58 | /// - parameter columnVisibility: An optional ``Binding`` to the current column visibility of the split view when the kind 59 | /// is ``AdaptiveTabViewContainerKind.sidebarView``. 60 | /// - parameter tabViews: A view builder to provide the views for the tabs. In ``AdaptiveTabViewContainerKind.tabView``, they appear as 61 | /// tabs within a ``NavigationView``. In ``AdaptiveTabViewContainerKind.sidebarView``, they appear at the top of the sidebar. You can 62 | /// use the ``AdaptiveTabViewContainerKind`` to conditionally show tab views. 63 | /// - parameter defaultContent: The default view to use in ``AdaptiveTabViewContainerKind.sidebarView`` in the content panel, before 64 | /// anything has been selected. This is only used when ``splitViewKind`` is ``AdaptiveTabViewSplitViewKind.threeColumn``. 65 | /// - parameter defaultDetail: The default view to use in ``AdaptiveTabViewContainerKind.sidebarView`` in the detail panel, before 66 | /// anything has been selected. 67 | /// - parameter sidebarExtraContent:A view builder to provide extra content in the ``List`` below the tab views when the kind 68 | /// is ``AdaptiveTabViewContainerKind.sidebarView``. 69 | public init( 70 | appName: String, 71 | selectedTab: Binding, 72 | splitViewKind: AdaptiveTabViewSplitViewKind = .threeColumn, 73 | columnVisibility: Binding? = nil, 74 | @SequenceBuilder tabViews: @escaping (AdaptiveTabViewContainerKind) -> TabContent, 75 | @ViewBuilder defaultContent: @escaping () -> DefaultContentView = { EmptyView() }, 76 | @ViewBuilder defaultDetail: @escaping () -> DefaultDetailView, 77 | @ViewBuilder sidebarExtraContent: @escaping () -> SidebarExtraContent = { EmptyView() } 78 | ) { 79 | self.appName = appName 80 | self._selectedTab = selectedTab 81 | self.splitViewKind = splitViewKind 82 | self.columnVisibilityBinding = columnVisibility 83 | self.tabViewBuilder = tabViews 84 | self.defaultContentBuilder = defaultContent 85 | self.defaultDetailBuilder = defaultDetail 86 | self.sidebarExtraContentBuilder = sidebarExtraContent 87 | 88 | self._selectedTabViewTab = State(initialValue: selectedTab.wrappedValue) 89 | self._selectedSidebarViewTab = State(initialValue: selectedTab.wrappedValue) 90 | } 91 | 92 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass 93 | 94 | public var body: some View { 95 | Group { 96 | switch horizontalSizeClass { 97 | case .compact: 98 | TabLayoutView( 99 | selectedTab: $selectedTabViewTab, 100 | tabViewBuilder 101 | ) 102 | default: 103 | SidebarLayoutView( 104 | appName, 105 | selectedTab: $selectedSidebarViewTab, 106 | splitViewKind: splitViewKind, 107 | columnVisibility: columnVisibilityBinding ?? $columnVisibilityState, 108 | tabViewBuilder: tabViewBuilder, 109 | defaultContentBuilder: defaultContentBuilder, 110 | defaultDetailBuilder: defaultDetailBuilder, 111 | sidebarExtraContent: sidebarExtraContentBuilder 112 | ) 113 | } 114 | } 115 | .onChange(of: horizontalSizeClass) { newValue in 116 | guard let containerKind = newValue?.containerKind else { return } 117 | 118 | selectedTab = selectedTabTransformer.transformer(containerKind, selectedTab) 119 | switch containerKind { 120 | case .tabView: 121 | selectedTabViewTab = selectedTab 122 | case .sidebarView: 123 | selectedSidebarViewTab = selectedTab 124 | } 125 | } 126 | .onChange(of: selectedTab) { newValue in 127 | switch horizontalSizeClass { 128 | case .compact: 129 | guard selectedTabViewTab != newValue else { return } 130 | selectedTabViewTab = newValue 131 | default: 132 | guard selectedSidebarViewTab != newValue else { return } 133 | selectedSidebarViewTab = newValue 134 | } 135 | } 136 | .onChange(of: selectedTabViewTab) { newValue in 137 | guard selectedTab != newValue else { return } 138 | selectedTab = newValue 139 | } 140 | .onChange(of: selectedSidebarViewTab) { newValue in 141 | guard selectedTab != newValue else { return } 142 | selectedTab = newValue 143 | } 144 | } 145 | } 146 | 147 | private extension UserInterfaceSizeClass { 148 | var containerKind: AdaptiveTabViewContainerKind { 149 | switch self { 150 | case .compact: return .tabView 151 | default: return .sidebarView 152 | } 153 | } 154 | } 155 | 156 | // MARK: - Previews 157 | 158 | struct AdaptiveTabView_Previews: PreviewProvider { 159 | @State static private var selectedTab = TabIdentifier("PreviewTitleImageProvidingView") 160 | 161 | static var previews: some View { 162 | AdaptiveTabView(appName: "AdaptiveTabView", selectedTab: $selectedTab) { (_) in 163 | PreviewTitleImageProvidingView() 164 | PreviewTitleImageProvidingView() 165 | PreviewTitleImageProvidingView() 166 | } defaultContent: { 167 | Text("Content") 168 | } defaultDetail: { 169 | Text("Detail") 170 | } sidebarExtraContent: { 171 | Text("Hello World") 172 | } 173 | .navigationSplitViewStyle(.balanced) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/Environment/SelectedTabTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectedTabTransformer.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-05-02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A struct meant to hold transformation logic for the selected tab when the ``AdaptiveTabView`` switches between different ``AdaptiveTabViewContainerKind``s. 11 | public struct SelectedTabTransformer { 12 | /// A closure to use to transform ``TabIdentifier``s between the different ``AdaptiveTabViewContainerKind``s. 13 | /// - parameter toKind: The container kind that the ``AdaptiveTabView`` will transform into. 14 | /// - parameter tabIdentifier: The ``TabIdentifier`` currently selected in the previous ``AdaptiveTabViewContainerKind``. 15 | public let transformer: (_ toKind: AdaptiveTabViewContainerKind, _ tabIdentifier: TabIdentifier) -> TabIdentifier 16 | 17 | public init(transformer: @escaping (AdaptiveTabViewContainerKind, TabIdentifier) -> TabIdentifier) { 18 | self.transformer = transformer 19 | } 20 | } 21 | 22 | extension SelectedTabTransformer: EnvironmentKey { 23 | public static var defaultValue = SelectedTabTransformer { (sizeClass, tabIdentifier) in 24 | return tabIdentifier 25 | } 26 | } 27 | 28 | public extension EnvironmentValues { 29 | /// An environment value that holds logic for transforming the selected tab in an ``AdaptiveTabView``. 30 | var selectedTabTransformer: SelectedTabTransformer { 31 | get {self[SelectedTabTransformer.self]} 32 | set {self[SelectedTabTransformer.self] = newValue} 33 | } 34 | } 35 | 36 | public extension View { 37 | /// Provide a transformer for converting the selected tab in an ``AdaptiveTabView`` when switching between ``AdaptiveTabViewContainerKind``s. 38 | /// - parameter transformer: The transformer to use when converting the selected tab. 39 | func selectedTabTransformer(_ transformer: SelectedTabTransformer) -> some View { 40 | environment(\.selectedTabTransformer, transformer) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/Extensions/Either+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Either+SwiftUI.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-25. 6 | // 7 | 8 | import SwiftUI 9 | import SequenceBuilder 10 | 11 | extension Either: TitleImageProviding where Left: TitleImageProviding, Right: TitleImageProviding { 12 | 13 | public var id: TabIdentifier { 14 | fold(left: \.id, right: \.id) 15 | } 16 | 17 | public var title: String { 18 | fold(left: \.title, right: \.title) 19 | } 20 | 21 | public var systemImageName: String { 22 | fold(left: \.systemImageName, right: \.systemImageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/Identifiers/TabIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabIdentifier.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-03-12. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - TabIdentifier 11 | 12 | public struct TabIdentifier: Identifiable, Hashable, Equatable, ExpressibleByStringLiteral { 13 | public let id: String 14 | 15 | public init(_ id: String) { 16 | self.id = id 17 | } 18 | 19 | public init(stringLiteral value: String) { 20 | id = value 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/PreviewContent/PreviewTitleImageProvidingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewTitleImageProvidingView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An internal struct used for SwiftUI Previews. 11 | struct PreviewTitleImageProvidingView: TabContentView { 12 | let title = "Preview View" 13 | let systemImageName = "doc.text.image" 14 | let id = TabIdentifier("PreviewTitleImageProvidingView") 15 | 16 | var body: some View { 17 | NavigationLink { 18 | Text("Destination") 19 | } label: { 20 | Text("Preview Content") 21 | } 22 | } 23 | } 24 | 25 | struct PreviewTitleImageProvidingView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | PreviewTitleImageProvidingView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/Protocols/TitleImageProviding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitlImageProviding.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A protocol to provide information for the tab item / sidebar item. 11 | public protocol TitleImageProviding { 12 | var id: TabIdentifier { get } 13 | /// The title for the screen. 14 | var title: String { get } 15 | /// The system image to use for the icon for the screen. 16 | var systemImageName: String { get } 17 | } 18 | 19 | extension TitleImageProviding { 20 | 21 | var label: some View { Label(title, systemImage: systemImageName) } 22 | } 23 | 24 | // MARK: - Previews 25 | 26 | private struct PreviewExampleView: TitleImageProviding { 27 | let title = "Settings" 28 | let systemImageName = "person" 29 | let id = TabIdentifier("PreviewExampleView") 30 | } 31 | 32 | struct TitleImageProviding_Previews: PreviewProvider { 33 | 34 | static var previews: some View { 35 | PreviewExampleView().label 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/SidebarLayout/SidebarItemNavigationLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarItemNavigationLink.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SidebarItemNavigationLink: View { 11 | 12 | let content: Content 13 | 14 | init( 15 | @ViewBuilder contentBuidler: () -> Content 16 | ) { 17 | self.content = contentBuidler() 18 | } 19 | 20 | var body: some View { 21 | NavigationLink(destination: content.navigationTitle(content.title)) { 22 | content.label 23 | } 24 | } 25 | } 26 | 27 | struct SidebarItemNavigationLink_Previews: PreviewProvider { 28 | static var previews: some View { 29 | SidebarItemNavigationLink { 30 | PreviewTitleImageProvidingView() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/SidebarLayout/SidebarLayoutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarLayoutView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | import SequenceBuilder 10 | 11 | struct SidebarLayoutView: View where TabContent.Element: TabContentView { 12 | 13 | private let appName: String 14 | private let selectedTab: Binding? 15 | private let splitViewKind: AdaptiveTabViewSplitViewKind 16 | private let columnVisibility: Binding 17 | private let tabViewBuilder: (AdaptiveTabViewContainerKind) -> TabContent 18 | private let defaultContentView: DefaultContentView 19 | private let defaultDetailView: DefaultDetailView 20 | private let sidebarExtraContent: () -> SidebarExtraContent 21 | 22 | init( 23 | _ appName: String, 24 | selectedTab: Binding?, 25 | splitViewKind: AdaptiveTabViewSplitViewKind, 26 | columnVisibility: Binding, 27 | @SequenceBuilder tabViewBuilder: @escaping (AdaptiveTabViewContainerKind) -> TabContent, 28 | @ViewBuilder defaultContentBuilder: () -> DefaultContentView, 29 | @ViewBuilder defaultDetailBuilder: () -> DefaultDetailView, 30 | @ViewBuilder sidebarExtraContent: @escaping () -> SidebarExtraContent 31 | ) { 32 | self.appName = appName 33 | self.selectedTab = selectedTab 34 | self.splitViewKind = splitViewKind 35 | self.columnVisibility = columnVisibility 36 | self.tabViewBuilder = tabViewBuilder 37 | self.defaultContentView = defaultContentBuilder() 38 | self.defaultDetailView = defaultDetailBuilder() 39 | self.sidebarExtraContent = sidebarExtraContent 40 | } 41 | 42 | var body: some View { 43 | switch splitViewKind { 44 | case .twoColumn: 45 | NavigationSplitView(columnVisibility: columnVisibility) { 46 | SidebarView( 47 | appName, 48 | selectedTab: selectedTab, 49 | tabViewBuilder: tabViewBuilder, 50 | sidebarExtraContent: sidebarExtraContent 51 | ) 52 | } detail: { 53 | defaultDetailView 54 | } 55 | case .threeColumn: 56 | NavigationSplitView(columnVisibility: columnVisibility) { 57 | SidebarView( 58 | appName, 59 | selectedTab: selectedTab, 60 | tabViewBuilder: tabViewBuilder, 61 | sidebarExtraContent: sidebarExtraContent 62 | ) 63 | } content: { 64 | defaultContentView 65 | } detail: { 66 | defaultDetailView 67 | } 68 | } 69 | } 70 | } 71 | 72 | struct SidebarLayoutView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | SidebarLayoutView( 75 | "AdaptiveTabView", 76 | selectedTab: nil, 77 | splitViewKind: .threeColumn, 78 | columnVisibility: .constant(.doubleColumn) 79 | ) { (_) in 80 | PreviewTitleImageProvidingView() 81 | } defaultContentBuilder: { 82 | Text("Content") 83 | } defaultDetailBuilder: { 84 | Text("Detail") 85 | } sidebarExtraContent: { 86 | Text("Hello World") 87 | } 88 | 89 | SidebarLayoutView( 90 | "AdaptiveTabView", 91 | selectedTab: nil, 92 | splitViewKind: .twoColumn, 93 | columnVisibility: .constant(.doubleColumn) 94 | ) { (_) in 95 | PreviewTitleImageProvidingView() 96 | } defaultContentBuilder: { 97 | Text("Content") 98 | } defaultDetailBuilder: { 99 | Text("Detail") 100 | } sidebarExtraContent: { 101 | Text("Hello World") 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/SidebarLayout/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | import SequenceBuilder 10 | 11 | // MARK: - SidebarView 12 | 13 | struct SidebarView: View where TabContent.Element: TabContentView { 14 | 15 | private let appName: String 16 | private let selectedTab: Binding? 17 | private let tabViewBuilder: (AdaptiveTabViewContainerKind) -> TabContent 18 | private let sidebarExtraContent: () -> SidebarExtraContent 19 | private let proxySelectedTab: Binding 20 | 21 | init( 22 | _ appName: String, 23 | selectedTab: Binding?, 24 | @SequenceBuilder tabViewBuilder: @escaping (AdaptiveTabViewContainerKind) -> TabContent, 25 | @ViewBuilder sidebarExtraContent: @escaping () -> SidebarExtraContent 26 | ) { 27 | self.appName = appName 28 | self.selectedTab = selectedTab 29 | self.tabViewBuilder = tabViewBuilder 30 | self.sidebarExtraContent = sidebarExtraContent 31 | 32 | self.proxySelectedTab = Binding { 33 | selectedTab?.wrappedValue 34 | } set: { (value, _) in 35 | if let value { 36 | selectedTab?.wrappedValue = value 37 | } 38 | } 39 | } 40 | 41 | var body: some View { 42 | List(selection: proxySelectedTab) { 43 | ForEach(sequence: tabViewBuilder(.sidebarView)) { (index, tabView) in 44 | SidebarItemNavigationLink { 45 | tabView 46 | } 47 | .tag(tabView.id) 48 | } 49 | 50 | sidebarExtraContent() 51 | } 52 | .listStyle(.sidebar) 53 | .navigationTitle(appName) 54 | } 55 | } 56 | 57 | // MARK: - Previews 58 | 59 | struct SidebarView_Previews: PreviewProvider { 60 | static var previews: some View { 61 | NavigationView { 62 | SidebarView("AdaptiveTabView", selectedTab: nil) { (_) in 63 | PreviewTitleImageProvidingView() 64 | } sidebarExtraContent: { 65 | Section("Other Content") { 66 | Text("Hello World") 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/TabLayout/TabLayoutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabLayoutView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | import SequenceBuilder 10 | 11 | struct TabLayoutView: View where TabContent.Element: TabContentView { 12 | 13 | private let selectedTab: Binding 14 | private let tabViews: TabContent 15 | 16 | init( 17 | selectedTab: Binding, 18 | @SequenceBuilder _ tabViewBuilder: (AdaptiveTabViewContainerKind) -> TabContent 19 | ) { 20 | self.selectedTab = selectedTab 21 | self.tabViews = tabViewBuilder(.tabView) 22 | } 23 | 24 | var body: some View { 25 | TabView(selection: selectedTab) { 26 | ForEach(sequence: tabViews) { (index, tabView) in 27 | TabNavigationView { 28 | tabView 29 | } 30 | .tag(tabView.id) 31 | } 32 | } 33 | } 34 | } 35 | 36 | struct TabLayoutView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | TabLayoutView(selectedTab: .constant("tabIdentifier")) { (_) in 39 | PreviewTitleImageProvidingView() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/TabLayout/TabNavigationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabNavigationView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabNavigationView: View { 11 | 12 | private let content: Content 13 | 14 | init(@ViewBuilder contentBuidler: () -> Content) { 15 | self.content = contentBuidler() 16 | } 17 | 18 | var body: some View { 19 | NavigationView { 20 | content 21 | .navigationTitle(content.title) 22 | } 23 | .tabItem { 24 | content.label 25 | } 26 | } 27 | } 28 | 29 | struct TabNavigationView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | TabView { 32 | TabNavigationView { 33 | PreviewTitleImageProvidingView() 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AdaptiveTabView/Typealiases/TabContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabContentView.swift 3 | // 4 | // 5 | // Created by Mark DiFranco on 2023-02-25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A SwiftUI ``View`` that also provides data for a tab item / sidebar item. 11 | public typealias TabContentView = View & TitleImageProviding 12 | -------------------------------------------------------------------------------- /Tests/AdaptableTabViewTests/AdaptableTabViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AdaptiveTabView 3 | 4 | final class AdaptiveTabViewTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(AdaptiveTabView().text, "Hello, World!") 10 | } 11 | } 12 | --------------------------------------------------------------------------------