├── .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://swiftpackageindex.com/mpdifran/AdaptiveTabView)
4 | [](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 | |||
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 |
--------------------------------------------------------------------------------