├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcuserdata
│ └── tbrennan.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Demo
├── SwiftUILayoutsDemo.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ └── tbrennan.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── SwiftUILayoutsDemo
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── demo0.imageset
│ │ ├── Contents.json
│ │ └── demo0.jpeg
│ ├── demo1.imageset
│ │ ├── Contents.json
│ │ └── demo1.jpeg
│ ├── demo2.imageset
│ │ ├── Contents.json
│ │ └── demo2.jpeg
│ ├── demo3.imageset
│ │ ├── Contents.json
│ │ └── demo3.jpeg
│ ├── demo4.imageset
│ │ ├── Contents.json
│ │ └── demo4.jpeg
│ ├── demo5.imageset
│ │ ├── Contents.json
│ │ └── demo5.jpeg
│ ├── demo6.imageset
│ │ ├── Contents.json
│ │ └── demo6.jpeg
│ └── demo7.imageset
│ │ ├── Contents.json
│ │ └── demo7.jpeg
│ ├── ContentView.swift
│ ├── FlowTestView.swift
│ ├── LayoutTypes.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── SwiftUILayoutsApp.swift
│ └── WaterfallTestView.swift
├── Package.swift
├── README.md
└── Sources
└── SwiftUILayouts
├── CompressingHStack.swift
├── EqualHStack.swift
├── EqualVStack.swift
├── FlowLayout.swift
└── VerticalWaterfallLayout.swift
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/tbrennan.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwiftUILayouts.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | SwiftUILayouts
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | D74C964C2855E58C00CEE4B9 /* SwiftUILayouts in Frameworks */ = {isa = PBXBuildFile; productRef = D74C964B2855E58C00CEE4B9 /* SwiftUILayouts */; };
11 | D74C964E2855E6B600CEE4B9 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = D74C964D2855E6B600CEE4B9 /* README.md */; };
12 | D7A30936285486B800413565 /* SwiftUILayoutsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A30935285486B800413565 /* SwiftUILayoutsApp.swift */; };
13 | D7A30938285486B800413565 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A30937285486B800413565 /* ContentView.swift */; };
14 | D7A3093A285486BA00413565 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D7A30939285486BA00413565 /* Assets.xcassets */; };
15 | D7A3093D285486BA00413565 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D7A3093C285486BA00413565 /* Preview Assets.xcassets */; };
16 | D7A30944285486C900413565 /* LayoutTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A30943285486C900413565 /* LayoutTypes.swift */; };
17 | D7A3094A28558F1E00413565 /* FlowTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A3094928558F1E00413565 /* FlowTestView.swift */; };
18 | D7A3094C285594A500413565 /* WaterfallTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A3094B285594A500413565 /* WaterfallTestView.swift */; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXFileReference section */
22 | D74C96472855E47700CEE4B9 /* SwiftUILayouts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftUILayouts; path = ..; sourceTree = ""; };
23 | D74C964D2855E6B600CEE4B9 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; };
24 | D7A30932285486B800413565 /* SwiftUILayoutsDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUILayoutsDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
25 | D7A30935285486B800413565 /* SwiftUILayoutsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUILayoutsApp.swift; sourceTree = ""; };
26 | D7A30937285486B800413565 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | D7A30939285486BA00413565 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
28 | D7A3093C285486BA00413565 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
29 | D7A30943285486C900413565 /* LayoutTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutTypes.swift; sourceTree = ""; };
30 | D7A3094928558F1E00413565 /* FlowTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTestView.swift; sourceTree = ""; };
31 | D7A3094B285594A500413565 /* WaterfallTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaterfallTestView.swift; sourceTree = ""; };
32 | /* End PBXFileReference section */
33 |
34 | /* Begin PBXFrameworksBuildPhase section */
35 | D7A3092F285486B800413565 /* Frameworks */ = {
36 | isa = PBXFrameworksBuildPhase;
37 | buildActionMask = 2147483647;
38 | files = (
39 | D74C964C2855E58C00CEE4B9 /* SwiftUILayouts in Frameworks */,
40 | );
41 | runOnlyForDeploymentPostprocessing = 0;
42 | };
43 | /* End PBXFrameworksBuildPhase section */
44 |
45 | /* Begin PBXGroup section */
46 | D74C964A2855E58C00CEE4B9 /* Frameworks */ = {
47 | isa = PBXGroup;
48 | children = (
49 | );
50 | name = Frameworks;
51 | sourceTree = "";
52 | };
53 | D7A30929285486B800413565 = {
54 | isa = PBXGroup;
55 | children = (
56 | D74C964D2855E6B600CEE4B9 /* README.md */,
57 | D7A3094F2855E1D200413565 /* Packages */,
58 | D7A30934285486B800413565 /* SwiftUILayoutsDemo */,
59 | D7A30933285486B800413565 /* Products */,
60 | D74C964A2855E58C00CEE4B9 /* Frameworks */,
61 | );
62 | sourceTree = "";
63 | };
64 | D7A30933285486B800413565 /* Products */ = {
65 | isa = PBXGroup;
66 | children = (
67 | D7A30932285486B800413565 /* SwiftUILayoutsDemo.app */,
68 | );
69 | name = Products;
70 | sourceTree = "";
71 | };
72 | D7A30934285486B800413565 /* SwiftUILayoutsDemo */ = {
73 | isa = PBXGroup;
74 | children = (
75 | D7A30935285486B800413565 /* SwiftUILayoutsApp.swift */,
76 | D7A30937285486B800413565 /* ContentView.swift */,
77 | D7A3094B285594A500413565 /* WaterfallTestView.swift */,
78 | D7A3094928558F1E00413565 /* FlowTestView.swift */,
79 | D7A30943285486C900413565 /* LayoutTypes.swift */,
80 | D7A30939285486BA00413565 /* Assets.xcassets */,
81 | D7A3093B285486BA00413565 /* Preview Content */,
82 | );
83 | path = SwiftUILayoutsDemo;
84 | sourceTree = "";
85 | };
86 | D7A3093B285486BA00413565 /* Preview Content */ = {
87 | isa = PBXGroup;
88 | children = (
89 | D7A3093C285486BA00413565 /* Preview Assets.xcassets */,
90 | );
91 | path = "Preview Content";
92 | sourceTree = "";
93 | };
94 | D7A3094F2855E1D200413565 /* Packages */ = {
95 | isa = PBXGroup;
96 | children = (
97 | D74C96472855E47700CEE4B9 /* SwiftUILayouts */,
98 | );
99 | name = Packages;
100 | sourceTree = "";
101 | };
102 | /* End PBXGroup section */
103 |
104 | /* Begin PBXNativeTarget section */
105 | D7A30931285486B800413565 /* SwiftUILayoutsDemo */ = {
106 | isa = PBXNativeTarget;
107 | buildConfigurationList = D7A30940285486BA00413565 /* Build configuration list for PBXNativeTarget "SwiftUILayoutsDemo" */;
108 | buildPhases = (
109 | D7A3092E285486B800413565 /* Sources */,
110 | D7A3092F285486B800413565 /* Frameworks */,
111 | D7A30930285486B800413565 /* Resources */,
112 | );
113 | buildRules = (
114 | );
115 | dependencies = (
116 | D74C96492855E49800CEE4B9 /* PBXTargetDependency */,
117 | );
118 | name = SwiftUILayoutsDemo;
119 | packageProductDependencies = (
120 | D74C964B2855E58C00CEE4B9 /* SwiftUILayouts */,
121 | );
122 | productName = SwiftUILayouts;
123 | productReference = D7A30932285486B800413565 /* SwiftUILayoutsDemo.app */;
124 | productType = "com.apple.product-type.application";
125 | };
126 | /* End PBXNativeTarget section */
127 |
128 | /* Begin PBXProject section */
129 | D7A3092A285486B800413565 /* Project object */ = {
130 | isa = PBXProject;
131 | attributes = {
132 | BuildIndependentTargetsInParallel = 1;
133 | LastSwiftUpdateCheck = 1400;
134 | LastUpgradeCheck = 1400;
135 | TargetAttributes = {
136 | D7A30931285486B800413565 = {
137 | CreatedOnToolsVersion = 14.0;
138 | };
139 | };
140 | };
141 | buildConfigurationList = D7A3092D285486B800413565 /* Build configuration list for PBXProject "SwiftUILayoutsDemo" */;
142 | compatibilityVersion = "Xcode 14.0";
143 | developmentRegion = en;
144 | hasScannedForEncodings = 0;
145 | knownRegions = (
146 | en,
147 | Base,
148 | );
149 | mainGroup = D7A30929285486B800413565;
150 | productRefGroup = D7A30933285486B800413565 /* Products */;
151 | projectDirPath = "";
152 | projectRoot = "";
153 | targets = (
154 | D7A30931285486B800413565 /* SwiftUILayoutsDemo */,
155 | );
156 | };
157 | /* End PBXProject section */
158 |
159 | /* Begin PBXResourcesBuildPhase section */
160 | D7A30930285486B800413565 /* Resources */ = {
161 | isa = PBXResourcesBuildPhase;
162 | buildActionMask = 2147483647;
163 | files = (
164 | D74C964E2855E6B600CEE4B9 /* README.md in Resources */,
165 | D7A3093D285486BA00413565 /* Preview Assets.xcassets in Resources */,
166 | D7A3093A285486BA00413565 /* Assets.xcassets in Resources */,
167 | );
168 | runOnlyForDeploymentPostprocessing = 0;
169 | };
170 | /* End PBXResourcesBuildPhase section */
171 |
172 | /* Begin PBXSourcesBuildPhase section */
173 | D7A3092E285486B800413565 /* Sources */ = {
174 | isa = PBXSourcesBuildPhase;
175 | buildActionMask = 2147483647;
176 | files = (
177 | D7A30944285486C900413565 /* LayoutTypes.swift in Sources */,
178 | D7A3094C285594A500413565 /* WaterfallTestView.swift in Sources */,
179 | D7A30938285486B800413565 /* ContentView.swift in Sources */,
180 | D7A3094A28558F1E00413565 /* FlowTestView.swift in Sources */,
181 | D7A30936285486B800413565 /* SwiftUILayoutsApp.swift in Sources */,
182 | );
183 | runOnlyForDeploymentPostprocessing = 0;
184 | };
185 | /* End PBXSourcesBuildPhase section */
186 |
187 | /* Begin PBXTargetDependency section */
188 | D74C96492855E49800CEE4B9 /* PBXTargetDependency */ = {
189 | isa = PBXTargetDependency;
190 | productRef = D74C96482855E49800CEE4B9 /* SwiftUILayouts */;
191 | };
192 | /* End PBXTargetDependency section */
193 |
194 | /* Begin XCBuildConfiguration section */
195 | D7A3093E285486BA00413565 /* Debug */ = {
196 | isa = XCBuildConfiguration;
197 | buildSettings = {
198 | ALWAYS_SEARCH_USER_PATHS = NO;
199 | CLANG_ANALYZER_NONNULL = YES;
200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
202 | CLANG_ENABLE_MODULES = YES;
203 | CLANG_ENABLE_OBJC_ARC = YES;
204 | CLANG_ENABLE_OBJC_WEAK = YES;
205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
206 | CLANG_WARN_BOOL_CONVERSION = YES;
207 | CLANG_WARN_COMMA = YES;
208 | CLANG_WARN_CONSTANT_CONVERSION = YES;
209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
212 | CLANG_WARN_EMPTY_BODY = YES;
213 | CLANG_WARN_ENUM_CONVERSION = YES;
214 | CLANG_WARN_INFINITE_RECURSION = YES;
215 | CLANG_WARN_INT_CONVERSION = YES;
216 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
217 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
220 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
222 | CLANG_WARN_STRICT_PROTOTYPES = YES;
223 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
225 | CLANG_WARN_UNREACHABLE_CODE = YES;
226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
227 | COPY_PHASE_STRIP = NO;
228 | DEBUG_INFORMATION_FORMAT = dwarf;
229 | ENABLE_STRICT_OBJC_MSGSEND = YES;
230 | ENABLE_TESTABILITY = YES;
231 | GCC_C_LANGUAGE_STANDARD = gnu11;
232 | GCC_DYNAMIC_NO_PIC = NO;
233 | GCC_NO_COMMON_BLOCKS = YES;
234 | GCC_OPTIMIZATION_LEVEL = 0;
235 | GCC_PREPROCESSOR_DEFINITIONS = (
236 | "DEBUG=1",
237 | "$(inherited)",
238 | );
239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
241 | GCC_WARN_UNDECLARED_SELECTOR = YES;
242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
243 | GCC_WARN_UNUSED_FUNCTION = YES;
244 | GCC_WARN_UNUSED_VARIABLE = YES;
245 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
246 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
247 | MTL_FAST_MATH = YES;
248 | ONLY_ACTIVE_ARCH = YES;
249 | SDKROOT = iphoneos;
250 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
251 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
252 | };
253 | name = Debug;
254 | };
255 | D7A3093F285486BA00413565 /* Release */ = {
256 | isa = XCBuildConfiguration;
257 | buildSettings = {
258 | ALWAYS_SEARCH_USER_PATHS = NO;
259 | CLANG_ANALYZER_NONNULL = YES;
260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
262 | CLANG_ENABLE_MODULES = YES;
263 | CLANG_ENABLE_OBJC_ARC = YES;
264 | CLANG_ENABLE_OBJC_WEAK = YES;
265 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
266 | CLANG_WARN_BOOL_CONVERSION = YES;
267 | CLANG_WARN_COMMA = YES;
268 | CLANG_WARN_CONSTANT_CONVERSION = YES;
269 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
270 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
271 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
272 | CLANG_WARN_EMPTY_BODY = YES;
273 | CLANG_WARN_ENUM_CONVERSION = YES;
274 | CLANG_WARN_INFINITE_RECURSION = YES;
275 | CLANG_WARN_INT_CONVERSION = YES;
276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
277 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
278 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
280 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
282 | CLANG_WARN_STRICT_PROTOTYPES = YES;
283 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
285 | CLANG_WARN_UNREACHABLE_CODE = YES;
286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
287 | COPY_PHASE_STRIP = NO;
288 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
289 | ENABLE_NS_ASSERTIONS = NO;
290 | ENABLE_STRICT_OBJC_MSGSEND = YES;
291 | GCC_C_LANGUAGE_STANDARD = gnu11;
292 | GCC_NO_COMMON_BLOCKS = YES;
293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
295 | GCC_WARN_UNDECLARED_SELECTOR = YES;
296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
297 | GCC_WARN_UNUSED_FUNCTION = YES;
298 | GCC_WARN_UNUSED_VARIABLE = YES;
299 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
300 | MTL_ENABLE_DEBUG_INFO = NO;
301 | MTL_FAST_MATH = YES;
302 | SDKROOT = iphoneos;
303 | SWIFT_COMPILATION_MODE = wholemodule;
304 | SWIFT_OPTIMIZATION_LEVEL = "-O";
305 | VALIDATE_PRODUCT = YES;
306 | };
307 | name = Release;
308 | };
309 | D7A30941285486BA00413565 /* Debug */ = {
310 | isa = XCBuildConfiguration;
311 | buildSettings = {
312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
313 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
314 | CODE_SIGN_STYLE = Automatic;
315 | CURRENT_PROJECT_VERSION = 1;
316 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUILayoutsDemo/Preview Content\"";
317 | DEVELOPMENT_TEAM = 2JV298SK2V;
318 | ENABLE_PREVIEWS = YES;
319 | GENERATE_INFOPLIST_FILE = YES;
320 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
321 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
322 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
323 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
324 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
325 | LD_RUNPATH_SEARCH_PATHS = (
326 | "$(inherited)",
327 | "@executable_path/Frameworks",
328 | );
329 | MARKETING_VERSION = 1.0;
330 | PRODUCT_BUNDLE_IDENTIFIER = com.apptekstudios.SwiftUILayouts;
331 | PRODUCT_NAME = "$(TARGET_NAME)";
332 | SWIFT_EMIT_LOC_STRINGS = YES;
333 | SWIFT_VERSION = 5.0;
334 | TARGETED_DEVICE_FAMILY = "1,2";
335 | };
336 | name = Debug;
337 | };
338 | D7A30942285486BA00413565 /* Release */ = {
339 | isa = XCBuildConfiguration;
340 | buildSettings = {
341 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
342 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
343 | CODE_SIGN_STYLE = Automatic;
344 | CURRENT_PROJECT_VERSION = 1;
345 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUILayoutsDemo/Preview Content\"";
346 | DEVELOPMENT_TEAM = 2JV298SK2V;
347 | ENABLE_PREVIEWS = YES;
348 | GENERATE_INFOPLIST_FILE = YES;
349 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
350 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
351 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
352 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
353 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
354 | LD_RUNPATH_SEARCH_PATHS = (
355 | "$(inherited)",
356 | "@executable_path/Frameworks",
357 | );
358 | MARKETING_VERSION = 1.0;
359 | PRODUCT_BUNDLE_IDENTIFIER = com.apptekstudios.SwiftUILayouts;
360 | PRODUCT_NAME = "$(TARGET_NAME)";
361 | SWIFT_EMIT_LOC_STRINGS = YES;
362 | SWIFT_VERSION = 5.0;
363 | TARGETED_DEVICE_FAMILY = "1,2";
364 | };
365 | name = Release;
366 | };
367 | /* End XCBuildConfiguration section */
368 |
369 | /* Begin XCConfigurationList section */
370 | D7A3092D285486B800413565 /* Build configuration list for PBXProject "SwiftUILayoutsDemo" */ = {
371 | isa = XCConfigurationList;
372 | buildConfigurations = (
373 | D7A3093E285486BA00413565 /* Debug */,
374 | D7A3093F285486BA00413565 /* Release */,
375 | );
376 | defaultConfigurationIsVisible = 0;
377 | defaultConfigurationName = Release;
378 | };
379 | D7A30940285486BA00413565 /* Build configuration list for PBXNativeTarget "SwiftUILayoutsDemo" */ = {
380 | isa = XCConfigurationList;
381 | buildConfigurations = (
382 | D7A30941285486BA00413565 /* Debug */,
383 | D7A30942285486BA00413565 /* Release */,
384 | );
385 | defaultConfigurationIsVisible = 0;
386 | defaultConfigurationName = Release;
387 | };
388 | /* End XCConfigurationList section */
389 |
390 | /* Begin XCSwiftPackageProductDependency section */
391 | D74C96482855E49800CEE4B9 /* SwiftUILayouts */ = {
392 | isa = XCSwiftPackageProductDependency;
393 | productName = SwiftUILayouts;
394 | };
395 | D74C964B2855E58C00CEE4B9 /* SwiftUILayouts */ = {
396 | isa = XCSwiftPackageProductDependency;
397 | productName = SwiftUILayouts;
398 | };
399 | /* End XCSwiftPackageProductDependency section */
400 | };
401 | rootObject = D7A3092A285486B800413565 /* Project object */;
402 | }
403 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo.xcodeproj/xcuserdata/tbrennan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo.xcodeproj/xcuserdata/tbrennan.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwiftUILayouts.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | SwiftUILayoutsDemo.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/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 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/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 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo0.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo0.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo0.imageset/demo0.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo0.imageset/demo0.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo1.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo1.imageset/demo1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo1.imageset/demo1.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo2.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo2.imageset/demo2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo2.imageset/demo2.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo3.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo3.imageset/demo3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo3.imageset/demo3.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo4.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo4.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo4.imageset/demo4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo4.imageset/demo4.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo5.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo5.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo5.imageset/demo5.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo5.imageset/demo5.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo6.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo6.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo6.imageset/demo6.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo6.imageset/demo6.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo7.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "demo7.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo7.imageset/demo7.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apptekstudios/SwiftUILayouts/de1da15d1afee3b41dc628b22a4cfef381d3986f/Demo/SwiftUILayoutsDemo/Assets.xcassets/demo7.imageset/demo7.jpeg
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 11/6/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | @State var currentLayout: LayoutType?
13 | @State var columnVisibility: NavigationSplitViewVisibility = .doubleColumn
14 |
15 | var body: some View {
16 | NavigationSplitView(columnVisibility: $columnVisibility) {
17 | List(LayoutType.allCases, selection: $currentLayout) { layout in
18 | Text(layout.title)
19 | }
20 | .navigationTitle("Layout Demos")
21 | } detail: {
22 | NavigationStack {
23 | DetailView(layoutType: currentLayout)
24 | }
25 | }
26 | .navigationSplitViewStyle(.balanced)
27 | }
28 | }
29 |
30 | enum LayoutType: String, Identifiable, CaseIterable {
31 | case flow
32 | case waterfall
33 |
34 | var id: Self { self }
35 | var title: String {
36 | switch self {
37 | case .flow: return "Flow Layout"
38 | case .waterfall: return "Waterfall Layout"
39 | }
40 | }
41 | }
42 |
43 | struct DetailView: View {
44 | var layoutType: LayoutType?
45 | var body: some View {
46 | switch layoutType {
47 | case .none: Text("Select a layout from the sidebar.")
48 | case .flow: FlowTestView()
49 | case .waterfall: WaterfallTestView()
50 |
51 | }
52 | }
53 | }
54 |
55 | struct ContentView_Previews: PreviewProvider {
56 | static var previews: some View {
57 | ContentView()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/FlowTestView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 11/6/2022.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUILayouts
10 |
11 | struct FlowTestView: View {
12 | @State var testContent = ["Hello World", "Custom layouts in SwiftUI wow!", "This is a very long string that takes up multiple lines in portrait mode", "String with\nline break", "Short text"]
13 | @State var horizontalAlignment: HorizontalAlignment = .leading
14 | @State var verticalAlignment: VerticalAlignment = .top
15 | var body: some View {
16 | ScrollView(.vertical) {
17 | VStack(spacing: 0) {
18 | AnyLayout(FlowLayout(alignment:.init(horizontal: horizontalAlignment, vertical: verticalAlignment))) {
19 | ForEach(testContent, id: \.self) { i in
20 | Text(i)
21 | .padding(6)
22 | .frame(maxHeight: .infinity)
23 | .background(
24 | RoundedRectangle(cornerRadius: 5, style: .continuous)
25 | .fill(Color(.secondarySystemBackground))
26 | )
27 | }
28 | }.padding(10)
29 | Divider()
30 | }
31 | .animation(.default, value: horizontalAlignment)
32 | .animation(.default, value: verticalAlignment)
33 | .animation(.default, value: testContent)
34 | }
35 | .safeAreaInset(edge: .top) {
36 | HStack {
37 | picker.pickerStyle(.menu)
38 | Spacer()
39 | Button("Shuffle") {
40 | testContent.shuffle()
41 | }
42 | }.padding().background(.thinMaterial)
43 | }
44 | .navigationTitle("Flow Layout")
45 | }
46 | var picker: some View {
47 | Picker("Alignment", selection: $horizontalAlignment) {
48 | Text("Leading").tag(HorizontalAlignment.leading)
49 | Text("Center").tag(HorizontalAlignment.center)
50 | Text("Trailing").tag(HorizontalAlignment.trailing)
51 | }.pickerStyle(.segmented)
52 | }
53 | }
54 |
55 | extension HorizontalAlignment: Hashable {
56 | public func hash(into hasher: inout Hasher) {
57 | hasher.combine(String(describing: self))
58 | }
59 | }
60 |
61 |
62 | struct FlowTestView_Previews: PreviewProvider {
63 | static var previews: some View {
64 | FlowTestView()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/LayoutTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutTypes.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 11/6/2022.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | enum LayoutTypes: String, CaseIterable {
12 | case HStack
13 | case VStack
14 | case Grid
15 | case Flow
16 | }
17 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/SwiftUILayoutsApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUILayoutsApp.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 11/6/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct SwiftUILayoutsApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Demo/SwiftUILayoutsDemo/WaterfallTestView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 11/6/2022.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUILayouts
10 |
11 | struct DemoItem: Identifiable, Equatable {
12 | var imageName: String
13 | var id = UUID()
14 | }
15 |
16 | struct WaterfallTestView: View {
17 | @State var testContent: [DemoItem] = (0...7).map { DemoItem(imageName: "demo\($0)") } // (0...10).flatMap { _ in (existing) }
18 | @State var columns: Int = 3
19 | var body: some View {
20 | ScrollView(.vertical) {
21 | VStack(spacing: 0) {
22 | AnyLayout(VerticalWaterfallLayout(columns: columns)) {
23 | ForEach(testContent) { item in
24 | Image(item.imageName)
25 | .resizable()
26 | .aspectRatio(contentMode: .fill)
27 | }
28 | }.padding(.horizontal, 10)
29 | Divider()
30 | }
31 | .animation(.default, value: columns)
32 | .animation(.default, value: testContent)
33 |
34 | }
35 | .safeAreaInset(edge: .top) {
36 | HStack {
37 | Stepper("Columns", value: $columns, in: 1...5)
38 | Spacer()
39 | Button("Shuffle") {
40 | testContent.shuffle()
41 | }
42 | }.padding().background(.thinMaterial)
43 | }
44 | .navigationTitle("Waterfall")
45 | }
46 | }
47 |
48 | struct WaterfallTestView_Previews: PreviewProvider {
49 | static var previews: some View {
50 | NavigationStack {
51 | WaterfallTestView()
52 | }
53 |
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/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: "SwiftUILayouts",
8 | platforms: [.iOS(.v16), .macOS(.v13)],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "SwiftUILayouts",
13 | targets: ["SwiftUILayouts"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "SwiftUILayouts",
24 | dependencies: []),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUILayouts
2 |
3 | A library of commonly requested layouts. Implemented using SwiftUI's native layout system.
4 |
5 | **NOTE: SwiftUILayouts requires iOS 16 or above, as this uses the new SwiftUI Layout system.**
6 |
7 | ### Why use these?
8 | - Native SwiftUI Layouts are fast and can be safely embedded anywhere in SwiftUI views
9 | - Each layout is self-contained within a file. Want to customise it? Just copy the code (and send us a pull request with improvements!)
10 |
11 | ### Check out the demo app to see them in action
12 |
13 | # Layouts
14 |
15 | ## Flow Layout
16 | Ideal for tag lists, amongst many other uses. Lay out views horizontally, wrapping to the next line when space runs out
17 |
18 | ## Waterfall Layout
19 | Great for presenting images of varying aspect ratios. Ensures columns are filled equally while preserving order.
20 |
21 |
22 | # Credit
23 | The flow layout code was heavily inspired by objc.io's great exploration of the new Layout system.
24 |
--------------------------------------------------------------------------------
/Sources/SwiftUILayouts/CompressingHStack.swift:
--------------------------------------------------------------------------------
1 | /*
2 | ADAPTED FROM APPLE EXAMPLE
3 |
4 | Abstract:
5 | A custom horizontal stack that offers all its subviews the width of its largest subview.
6 | */
7 |
8 | import SwiftUI
9 |
10 | public struct CompressingHStack: Layout {
11 | public init(spacing: Double? = nil) {
12 | self.spacing = spacing
13 | }
14 |
15 | var spacing: Double?
16 |
17 | /// Returns a size that the layout container needs to arrange its subviews
18 | /// horizontally.
19 | /// - Tag: sizeThatFitsHorizontal
20 | public func sizeThatFits(
21 | proposal: ProposedViewSize,
22 | subviews: Subviews,
23 | cache: inout Void
24 | ) -> CGSize {
25 | guard !subviews.isEmpty else { return .zero }
26 |
27 | let maxSize = maxSize(subviews: subviews)
28 | let sizes = subviews.map { subview in
29 | calcSize(for: subview, targetSize: maxSize)
30 | }
31 | let spacing = spacing(subviews: subviews)
32 | let totalSpacing = spacing.reduce(0) { $0 + $1 }
33 |
34 | return CGSize(
35 | width: sizes.map(\.width).reduce(into: .zero, { $0 += $1}) + totalSpacing,
36 | height: sizes.map(\.height).max() ?? .zero)
37 | }
38 |
39 | /// Places the subviews in a horizontal stack.
40 | /// - Tag: placeSubviewsHorizontal
41 | public func placeSubviews(
42 | in bounds: CGRect,
43 | proposal: ProposedViewSize,
44 | subviews: Subviews,
45 | cache: inout Void
46 | ) {
47 | guard !subviews.isEmpty else { return }
48 |
49 | let maxSize = maxSize(subviews: subviews)
50 | let spacing = spacing(subviews: subviews)
51 |
52 | var nextX = bounds.minX
53 |
54 | for index in subviews.indices {
55 | let size = calcSize(for: subviews[index], targetSize: maxSize)
56 | let proposal = ProposedViewSize(size)
57 | subviews[index].place(
58 | at: CGPoint(x: nextX + size.width/2, y: bounds.midY),
59 | anchor: .center,
60 | proposal: proposal)
61 | nextX += size.width + spacing[index]
62 | }
63 | }
64 |
65 | private func calcSize(for subview: LayoutSubview, targetSize: CGSize) -> CGSize {
66 | return subview.sizeThatFits(ProposedViewSize(width: nil, height: targetSize.height))
67 | }
68 |
69 | /// Finds the largest ideal size of the subviews.
70 | private func maxSize(subviews: Subviews) -> CGSize {
71 | let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
72 | let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in
73 | CGSize(
74 | width: max(currentMax.width, subviewSize.width),
75 | height: max(currentMax.height, subviewSize.height))
76 | }
77 |
78 | return maxSize
79 | }
80 |
81 | /// Gets an array of preferred spacing sizes between subviews in the
82 | /// horizontal dimension.
83 | private func spacing(subviews: Subviews) -> [CGFloat] {
84 | subviews.indices.map { index in
85 | guard index < subviews.count - 1 else { return 0 }
86 | return spacing ?? subviews[index].spacing.distance(
87 | to: subviews[index + 1].spacing,
88 | along: .horizontal)
89 | }
90 | }
91 |
92 | public static var layoutProperties: LayoutProperties {
93 | var properties = LayoutProperties()
94 | properties.stackOrientation = .horizontal
95 | return properties
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/SwiftUILayouts/EqualHStack.swift:
--------------------------------------------------------------------------------
1 | /*
2 | ADAPTED FROM APPLE EXAMPLE
3 |
4 | Abstract:
5 | A custom horizontal stack that offers all its subviews the width of its largest subview.
6 | */
7 |
8 | import SwiftUI
9 |
10 | /// A custom horizontal stack that offers all its subviews the width of its
11 | /// widest subview.
12 | ///
13 | /// This custom layout arranges views horizontally, giving each the width needed
14 | /// by the widest subview.
15 | ///
16 | /// 
20 | ///
21 | /// The custom stack implements the protocol's two required methods. First,
22 | /// ``sizeThatFits(proposal:subviews:cache:)`` reports the container's size,
23 | /// given a set of subviews.
24 | ///
25 | /// ```swift
26 | /// let maxSize = maxSize(subviews: subviews)
27 | /// let spacing = spacing(subviews: subviews)
28 | /// let totalSpacing = spacing.reduce(0) { $0 + $1 }
29 | ///
30 | /// return CGSize(
31 | /// width: maxSize.width * CGFloat(subviews.count) + totalSpacing,
32 | /// height: maxSize.height)
33 | /// ```
34 | ///
35 | /// This method combines the largest size in each dimension with the horizontal
36 | /// spacing between subviews to find the container's total size. Then,
37 | /// ``placeSubviews(in:proposal:subviews:cache:)`` tells each of the subviews
38 | /// where to appear within the layout's bounds.
39 | ///
40 | /// ```swift
41 | /// let maxSize = maxSize(subviews: subviews)
42 | /// let spacing = spacing(subviews: subviews)
43 | ///
44 | /// let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)
45 | /// var nextX = bounds.minX + maxSize.width / 2
46 | ///
47 | /// for index in subviews.indices {
48 | /// subviews[index].place(
49 | /// at: CGPoint(x: nextX, y: bounds.midY),
50 | /// anchor: .center,
51 | /// proposal: placementProposal)
52 | /// nextX += maxSize.width + spacing[index]
53 | /// }
54 | /// ```
55 | ///
56 | /// The method creates a single size proposal for the subviews, and then uses
57 | /// that, along with a point that changes for each subview, to arrange the
58 | /// subviews in a horizontal line with default spacing.
59 | public struct EqualHStack: Layout {
60 | public init(spacing: Double? = nil, fillAvailable: Bool = false) {
61 | self.spacing = spacing
62 | self.fillAvailable = fillAvailable
63 | }
64 |
65 | var spacing: Double?
66 | var fillAvailable: Bool
67 |
68 | /// Returns a size that the layout container needs to arrange its subviews
69 | /// horizontally.
70 | /// - Tag: sizeThatFitsHorizontal
71 | public func sizeThatFits(
72 | proposal: ProposedViewSize,
73 | subviews: Subviews,
74 | cache: inout Void
75 | ) -> CGSize {
76 | guard !subviews.isEmpty else { return .zero }
77 |
78 | let spacing = spacing(subviews: subviews)
79 | let totalSpacing = spacing.reduce(0) { $0 + $1 }
80 | let maxSize = maxSize(subviews: subviews)
81 |
82 | if fillAvailable, let width = proposal.width {
83 | return CGSize(
84 | width: width,
85 | height: maxSize.height)
86 | } else {
87 | let sumWidth = subviews.reduce(into: Double.zero) { partialResult, subview in
88 | partialResult += calcSize(for: subview, targetSize: maxSize).width
89 | }
90 |
91 | return CGSize(
92 | width: sumWidth + totalSpacing,
93 | height: maxSize.height)
94 | }
95 | }
96 |
97 | /// Places the subviews in a horizontal stack.
98 | /// - Tag: placeSubviewsHorizontal
99 | public func placeSubviews(
100 | in bounds: CGRect,
101 | proposal: ProposedViewSize,
102 | subviews: Subviews,
103 | cache: inout Void
104 | ) {
105 | guard !subviews.isEmpty else { return }
106 |
107 | let maxSize = maxSize(subviews: subviews)
108 | let spacing = spacing(subviews: subviews)
109 | var nextX = bounds.minX
110 |
111 | if fillAvailable, let width = proposal.width {
112 | let interim = subviews.indices.compactMap { index in
113 | InterimResult(subview: subviews[index], isNonExpandableWithWidth: isNonExpandableView(subviews[index]))
114 | }
115 | let nonExpandableSpace = interim.reduce(into: Double.zero) { partialResult, item in
116 | if let itemWidth = item.isNonExpandableWithWidth {
117 | partialResult += itemWidth
118 | }
119 | }
120 | let expandableCount = interim.reduce(into: Int.zero) { partialResult, item in
121 | if item.isNonExpandableWithWidth == nil {
122 | partialResult += 1
123 | }
124 | }
125 | let targetWidth = (expandableCount == .zero) ? 0 : (width - nonExpandableSpace) / Double(expandableCount)
126 | for index in subviews.indices {
127 | let size = calcSize(for: subviews[index], targetSize: CGSize(width: targetWidth, height: maxSize.height))
128 | let proposal = ProposedViewSize(size)
129 | subviews[index].place(
130 | at: CGPoint(x: nextX + size.width/2, y: bounds.midY),
131 | anchor: .center,
132 | proposal: proposal)
133 | nextX += size.width + spacing[index]
134 | }
135 | } else {
136 | for index in subviews.indices {
137 | let size = calcSize(for: subviews[index], targetSize: maxSize)
138 | let proposal = ProposedViewSize(size)
139 | subviews[index].place(
140 | at: CGPoint(x: nextX + size.width/2, y: bounds.midY),
141 | anchor: .center,
142 | proposal: proposal)
143 | nextX += size.width + spacing[index]
144 | }
145 | }
146 | }
147 |
148 | private func calcSize(for subview: LayoutSubview, targetSize: CGSize) -> CGSize {
149 | let actualSize = isNonExpandableView(subview) ?? targetSize.width
150 | return CGSize(width: actualSize, height: targetSize.height)
151 | }
152 |
153 | private func isNonExpandableView(_ subview: LayoutSubview) -> Double? {
154 | let itemMaxWidth = subview.sizeThatFits(.infinity).width
155 | return itemMaxWidth <= 1 ? itemMaxWidth : nil
156 | }
157 |
158 | /// Finds the largest ideal size of the subviews.
159 | private func maxSize(subviews: Subviews) -> CGSize {
160 | let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
161 | let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in
162 | CGSize(
163 | width: max(currentMax.width, subviewSize.width),
164 | height: max(currentMax.height, subviewSize.height))
165 | }
166 |
167 | return maxSize
168 | }
169 |
170 | /// Gets an array of preferred spacing sizes between subviews in the
171 | /// horizontal dimension.
172 | private func spacing(subviews: Subviews) -> [CGFloat] {
173 | subviews.indices.map { index in
174 | guard index < subviews.count - 1 else { return 0 }
175 | return spacing ?? subviews[index].spacing.distance(
176 | to: subviews[index + 1].spacing,
177 | along: .horizontal)
178 | }
179 | }
180 |
181 | public static var layoutProperties: LayoutProperties {
182 | var properties = LayoutProperties()
183 | properties.stackOrientation = .horizontal
184 | return properties
185 | }
186 |
187 | struct InterimResult {
188 | var subview: LayoutSubview
189 | var isNonExpandableWithWidth: Double?
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/Sources/SwiftUILayouts/EqualVStack.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | A custom vertical stack that offers all its subviews the width of its largest subview.
6 | */
7 |
8 | import SwiftUI
9 |
10 | /// A custom vertical stack that offers all its subviews the width of its
11 | /// widest subview.
12 | ///
13 | /// This custom layout behaves almost identically to the ``MyEqualWidthHStack``,
14 | /// except that it arranges equal-width subviews in a vertical stack, rather
15 | /// than a horizontal one. It also implements a cache.
16 | ///
17 | /// ### Adding a cache
18 | ///
19 | /// The methods of the
20 | /// [`Layout`](https://developer.apple.com/documentation/swiftui/layout)
21 | /// protocol take a bidirectional `cache`
22 | /// parameter. The cache provides access to optional storage that's shared among
23 | /// all the methods of a particular layout instance. To demonstrate the use of a
24 | /// cache, this layout creates storage to share size and spacing calculations
25 | /// between its ``sizeThatFits(proposal:subviews:cache:)`` and
26 | /// ``placeSubviews(in:proposal:subviews:cache:)`` implementations.
27 | ///
28 | /// First, the layout defines a ``CacheData`` type for the storage:
29 | ///
30 | /// ```swift
31 | /// struct CacheData {
32 | /// let maxSize: CGSize
33 | /// let spacing: [CGFloat]
34 | /// let totalSpacing: CGFloat
35 | /// }
36 | /// ```
37 | ///
38 | /// It then implements the protocol's optional ``makeCache(subviews:)``
39 | /// method to do the calculations for a set of subviews, returning a value of
40 | /// the type defined above.
41 | ///
42 | /// ```swift
43 | /// func makeCache(subviews: Subviews) -> CacheData {
44 | /// let maxSize = maxSize(subviews: subviews)
45 | /// let spacing = spacing(subviews: subviews)
46 | /// let totalSpacing = spacing.reduce(0) { $0 + $1 }
47 | ///
48 | /// return CacheData(
49 | /// maxSize: maxSize,
50 | /// spacing: spacing,
51 | /// totalSpacing: totalSpacing)
52 | /// }
53 | /// ```
54 | ///
55 | /// If the subviews change, SwiftUI calls the layout's
56 | /// ``updateCache(_:subviews:)`` method. The default implementation of that
57 | /// method calls ``makeCache(subviews:)`` again, which recalculates the data.
58 | /// Then the ``sizeThatFits(proposal:subviews:cache:)`` and
59 | /// ``placeSubviews(in:proposal:subviews:cache:)`` methods make
60 | /// use of their `cache` parameter to retrieve the data. For example,
61 | /// ``placeSubviews(in:proposal:subviews:cache:)`` reads the size and the
62 | /// spacing array from the cache.
63 | ///
64 | /// ```swift
65 | /// let maxSize = cache.maxSize
66 | /// let spacing = cache.spacing
67 | /// ```
68 | ///
69 | /// Contrast this with ``MyEqualWidthHStack``, which doesn't use a
70 | /// cache, and instead calculates the size and spacing information every time
71 | /// it needs that information.
72 | ///
73 | /// > Note: Most simple layouts, including this one, don't
74 | /// gain much efficiency from using a cache. You can profile your app
75 | /// with Instruments to find out whether a particular layout type actually
76 | /// benefits from a cache.
77 | public struct EqualVStack: Layout {
78 | public init(spacing: Double? = nil) {
79 | self.spacing = spacing
80 | }
81 |
82 | var spacing: Double?
83 |
84 | /// Returns a size that the layout container needs to arrange its subviews
85 | /// vertically with equal widths.
86 | public func sizeThatFits(
87 | proposal: ProposedViewSize,
88 | subviews: Subviews,
89 | cache: inout CacheData
90 | ) -> CGSize {
91 | guard !subviews.isEmpty else { return .zero }
92 |
93 | // Load size and spacing information from the cache.
94 | let maxSize = cache.maxSize
95 | let totalSpacing = cache.totalSpacing
96 |
97 | return CGSize(
98 | width: maxSize.width,
99 | height: maxSize.height * CGFloat(subviews.count) + totalSpacing)
100 | }
101 |
102 | /// Places the subviews in a vertical stack.
103 | /// - Tag: placeSubviewsVertical
104 | public func placeSubviews(
105 | in bounds: CGRect,
106 | proposal: ProposedViewSize,
107 | subviews: Subviews,
108 | cache: inout CacheData
109 | ) {
110 | guard !subviews.isEmpty else { return }
111 |
112 | // Load size and spacing information from the cache.
113 | let maxSize = cache.maxSize
114 | let spacing = cache.spacing
115 |
116 | let placementProposal = ProposedViewSize(width: maxSize.width, height: bounds.height)
117 | var nextY = bounds.minY + maxSize.height / 2
118 |
119 | for index in subviews.indices {
120 | subviews[index].place(
121 | at: CGPoint(x: bounds.midX, y: nextY),
122 | anchor: .center,
123 | proposal: placementProposal)
124 | nextY += maxSize.height + spacing[index]
125 | }
126 | }
127 |
128 | /// A type that stores cached data.
129 | /// - Tag: CacheData
130 | public struct CacheData {
131 | let maxSize: CGSize
132 | let spacing: [CGFloat]
133 | let totalSpacing: CGFloat
134 | }
135 |
136 | /// Creates a cache for a given set of subviews.
137 | ///
138 | /// When the subviews change, SwiftUI calls the ``updateCache(_:subviews:)``
139 | /// method. The ``EqualVStack`` layout relies on the default
140 | /// implementation of that method, which just calls this method again
141 | /// to recreate the cache.
142 | /// - Tag: makeCache
143 | public func makeCache(subviews: Subviews) -> CacheData {
144 | let maxSize = maxSize(subviews: subviews)
145 | let spacing = spacing(subviews: subviews)
146 | let totalSpacing = spacing.reduce(0) { $0 + $1 }
147 |
148 | return CacheData(
149 | maxSize: maxSize,
150 | spacing: spacing,
151 | totalSpacing: totalSpacing)
152 | }
153 |
154 | /// Finds the largest ideal size of the subviews.
155 | private func maxSize(subviews: Subviews) -> CGSize {
156 | let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
157 | let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in
158 | CGSize(
159 | width: max(currentMax.width, subviewSize.width),
160 | height: max(currentMax.height, subviewSize.height))
161 | }
162 |
163 | return maxSize
164 | }
165 |
166 | /// Gets an array of preferred spacing sizes between subviews in the
167 | /// vertical dimension.
168 | private func spacing(subviews: Subviews) -> [CGFloat] {
169 | subviews.indices.map { index in
170 | guard index < subviews.count - 1 else { return 0 }
171 |
172 | return spacing ?? subviews[index].spacing.distance(
173 | to: subviews[index + 1].spacing,
174 | along: .vertical)
175 | }
176 | }
177 | public static var layoutProperties: LayoutProperties {
178 | var properties = LayoutProperties()
179 | properties.stackOrientation = .vertical
180 | return properties
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Sources/SwiftUILayouts/FlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlowLayout.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 11/6/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct FlowLayout: Layout {
11 | var alignment: Alignment
12 | var spacingX: Double
13 | var spacingY: Double
14 | // All items in line offered the height of the largest item
15 | var fillLineHeight: Bool
16 |
17 | public init(alignment: Alignment = .leading, spacingX: Double = 10, spacingY: Double = 10, fillLineHeight: Bool = false) {
18 | self.alignment = alignment
19 | self.spacingX = spacingX
20 | self.spacingY = spacingY
21 | self.fillLineHeight = fillLineHeight
22 | }
23 |
24 | public struct LayoutCache {
25 | // If this changes invalidate the cache
26 | var targetContainerWidth: Double
27 | var items: [Int: CacheItem] = [:]
28 | var size: CGSize = .zero
29 |
30 | func ifValidForSize(_ width: Double) -> Self? {
31 | guard targetContainerWidth == width else { return nil }
32 | return self
33 | }
34 | }
35 | struct Line {
36 | var y: Double
37 | var height: Double = 0
38 | var width: Double = 0
39 | var maxY: Double { y + height }
40 | var items: [Int: CacheItem] = [:]
41 |
42 | mutating func applyAlignment(_ alignment: Alignment, layoutWidth: Double, fillLineHeight: Bool) {
43 | if fillLineHeight {
44 | for (index, _) in items {
45 | items[index]?.position.y = y
46 | items[index]?.size.height = height
47 | }
48 | } else {
49 | switch alignment.vertical {
50 | case .center:
51 | let centerY = y + (height / 2)
52 | for (index, item) in items {
53 | items[index]?.position.y = centerY - item.size.height / 2
54 | }
55 | case .bottom:
56 | let bottomY = y + height
57 | for (index, item) in items {
58 | items[index]?.position.y = bottomY - item.size.height
59 | }
60 | default: break
61 | }
62 | }
63 | switch alignment.horizontal {
64 | case .center:
65 | let xOffset = (layoutWidth - width) / 2
66 | for index in items.keys {
67 | items[index]?.position.x += xOffset
68 | }
69 | case .trailing:
70 | let xOffset = (layoutWidth - width)
71 | for index in items.keys {
72 | items[index]?.position.x += xOffset
73 | }
74 | default: break
75 | }
76 | }
77 | }
78 | struct CacheItem {
79 | var position: CGPoint
80 | var size: CGSize
81 | }
82 |
83 | public func makeCache(subviews: Subviews) -> LayoutCache? {
84 | return nil
85 | }
86 |
87 | public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout LayoutCache?) -> CGSize {
88 | let containerWidth = proposal.replacingUnspecifiedDimensions().width
89 | let calc = layout(subviews: subviews, containerWidth: containerWidth)
90 | cache = calc
91 | return calc.size
92 | }
93 |
94 | public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout LayoutCache?) {
95 | let calc = cache?.ifValidForSize(proposal.replacingUnspecifiedDimensions().width) ?? layout(subviews: subviews, containerWidth: bounds.width)
96 | for (index, subview) in zip(subviews.indices, subviews) {
97 | if let value = calc.items[index] {
98 | subview.place(at: bounds.origin + value.position,
99 | proposal: .init(value.size))
100 | }
101 | }
102 | }
103 |
104 | func layout(subviews: Subviews, containerWidth: CGFloat) -> LayoutCache {
105 | var result: LayoutCache = .init(targetContainerWidth: containerWidth)
106 | var currentPosition: CGPoint = .zero
107 | var currentLineHeight: CGFloat = 0
108 | var maxX: CGFloat = 0
109 | var lines: [Line] = [Line(y: 0)]
110 | for (index, subview) in zip(subviews.indices, subviews) {
111 | let size = subview.sizeThatFits(.init(width: containerWidth, height: nil))
112 | if currentPosition.x + spacingX + size.width > containerWidth {
113 | currentLineHeight = 0
114 | currentPosition.x = 0
115 | currentPosition.y += lines[lines.endIndex - 1].height + spacingY
116 | lines.append(Line(y: currentPosition.y))
117 | } else if lines.last?.items.isEmpty != true {
118 | currentPosition.x += spacingX
119 | }
120 | lines[lines.endIndex - 1].items[index] = .init(position: currentPosition, size: size)
121 | currentPosition.x += size.width
122 | maxX = min(containerWidth, max(maxX, currentPosition.x))
123 | currentLineHeight = max(currentLineHeight, size.height)
124 | lines[lines.endIndex - 1].width = currentPosition.x
125 | lines[lines.endIndex - 1].height = currentLineHeight
126 | }
127 | for index in lines.indices {
128 | lines[index].applyAlignment(alignment, layoutWidth: maxX, fillLineHeight: fillLineHeight)
129 | }
130 | result.size = CGSize(width: maxX, height: lines.last?.maxY ?? 0)
131 | result.items = lines.reduce(into: [Int: CacheItem](), { partialResult, line in
132 | partialResult.merge(line.items, uniquingKeysWith: {$1})
133 | })
134 | return result
135 | }
136 | public static var layoutProperties: LayoutProperties {
137 | var properties = LayoutProperties()
138 | properties.stackOrientation = .horizontal
139 | return properties
140 | }
141 | }
142 |
143 | func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
144 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
145 | }
146 |
--------------------------------------------------------------------------------
/Sources/SwiftUILayouts/VerticalWaterfallLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerticalWaterfallLayout.swift
3 | // SwiftUILayouts
4 | //
5 | // Created by T Brennan on 12/6/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct VerticalWaterfallLayout: Layout {
11 | var columns: Int
12 | var spacingX: Double
13 | var spacingY: Double
14 |
15 | public init(columns: Int = 3, spacingX: Double = 10, spacingY: Double = 10) {
16 | self.columns = columns
17 | self.spacingX = spacingX
18 | self.spacingY = spacingY
19 | }
20 |
21 | public struct LayoutCache {
22 | // If this changes invalidate the cache
23 | var targetContainerWidth: Double
24 | // If this changes invalidate the cache
25 | var columnCount: Int
26 | var items: [Int: CacheItem] = [:]
27 | var size: CGSize = .zero
28 |
29 | func ifValidForParams(_ width: Double, columns: Int) -> Self? {
30 | guard targetContainerWidth == width,
31 | columnCount == columns
32 | else { return nil }
33 | return self
34 | }
35 | }
36 | struct Column {
37 | var height: Double = 0
38 | var width: Double = 0
39 | var items: [Int: CacheItem] = [:]
40 | }
41 | struct CacheItem {
42 | var position: CGPoint
43 | var size: CGSize
44 | }
45 |
46 | public func makeCache(subviews: Subviews) -> LayoutCache? {
47 | return nil
48 | }
49 |
50 | public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout LayoutCache?) -> CGSize {
51 | let containerWidth = proposal.replacingUnspecifiedDimensions().width
52 | let calc = layout(subviews: subviews, containerWidth: containerWidth)
53 | cache = calc
54 | return calc.size
55 | }
56 |
57 | public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout LayoutCache?) {
58 | let calc = cache?.ifValidForParams(proposal.replacingUnspecifiedDimensions().width, columns: columns) ?? layout(subviews: subviews, containerWidth: bounds.width)
59 | for (index, subview) in zip(subviews.indices, subviews) {
60 | if let value = calc.items[index] {
61 | subview.place(at: bounds.origin + value.position,
62 | proposal: .init(value.size))
63 | }
64 | }
65 | }
66 |
67 | func layout(subviews: Subviews, containerWidth: CGFloat) -> LayoutCache {
68 | guard containerWidth != 0 else {return LayoutCache(targetContainerWidth: 0, columnCount: columns)}
69 | var result: LayoutCache = .init(targetContainerWidth: containerWidth, columnCount: columns)
70 | let columnWidth = (containerWidth - Double(columns - 1) * spacingX) / Double(columns)
71 | var columns: [Column] = .init(repeating: Column(width: columnWidth), count: columns)
72 | for (index, subview) in zip(subviews.indices, subviews) {
73 | let size = subview.sizeThatFits(.init(width: columnWidth, height: nil))
74 | let smallestColumnIndex = zip(columns, columns.indices).min(by: { $0.0.height < $1.0.height })?.1 ?? 0
75 | var currentColumn: Column {
76 | get { columns[smallestColumnIndex] }
77 | set { columns[smallestColumnIndex] = newValue }
78 | }
79 | let x = (columnWidth + spacingX) * Double(smallestColumnIndex)
80 | let y = currentColumn.height + spacingY
81 | let item = CacheItem(position: CGPoint(x: x, y: y), size: size)
82 | currentColumn.items[index] = item
83 | currentColumn.height = currentColumn.height + spacingY + item.size.height
84 | }
85 | let maxHeight = columns.max(by: { $0.height < $1.height })?.height ?? .zero
86 | result.size = CGSize(width: containerWidth, height: maxHeight)
87 | result.items = columns.reduce(into: [Int: CacheItem](), { partialResult, line in
88 | partialResult.merge(line.items, uniquingKeysWith: {$1})
89 | })
90 | return result
91 | }
92 | }
93 |
--------------------------------------------------------------------------------