├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Resources
└── Demo.gif
└── Sources
└── SwiftUIMasonry
├── API
├── HMasonry.swift
├── Masonry.swift
├── MasonryLines.swift
├── MasonryPlacementMode.swift
└── VMasonry.swift
└── Internal
├── EnvironmentValues+.swift
├── MasonryDynamicViewBuilder.swift
└── MasonryLayout.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Ciaran O'Brien
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.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 |
3 | /**
4 | * SwiftUIMasonry
5 | * Copyright (c) Ciaran O'Brien 2022
6 | * MIT license, see LICENSE file for details
7 | */
8 |
9 | import PackageDescription
10 |
11 | let package = Package(
12 | name: "SwiftUIMasonry",
13 | platforms: [
14 | .iOS(.v14),
15 | .macCatalyst(.v14),
16 | .macOS(.v11),
17 | .tvOS(.v14),
18 | .watchOS(.v7)
19 | ],
20 | products: [
21 | .library(
22 | name: "SwiftUIMasonry",
23 | targets: ["SwiftUIMasonry"]),
24 | ],
25 | dependencies: [],
26 | targets: [
27 | .target(
28 | name: "SwiftUIMasonry",
29 | dependencies: [])
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI Masonry
2 |
3 | SwiftUI views that arrange their children in a Pinterest-like layout.
4 |
5 | 
6 |
7 | ## HMasonry
8 | A view that arranges its children in a horizontal masonry.
9 |
10 | ### Usage
11 | ```swift
12 | ScrollView(.horizontal) {
13 | HMasonry(rows: 2) {
14 | //Masonry content
15 | }
16 | }
17 | ```
18 |
19 | ### Parameters
20 | * `rows`: The number of rows in the masonry.
21 | * `spacing`: The distance between adjacent subviews, or `nil` if you want the masonry to choose a default distance for each pair of subviews.
22 | * `content`: A view builder that creates the content of this masonry.
23 |
24 | ## VMasonry
25 | A view that arranges its children in a vertical masonry.
26 |
27 | ### Usage
28 | ```swift
29 | ScrollView(.vertical) {
30 | VMasonry(columns: 2) {
31 | //Masonry content
32 | }
33 | }
34 | ```
35 |
36 | ### Parameters
37 | * `columns`: The number of columns in the masonry.
38 | * `spacing`: The distance between adjacent subviews, or `nil` if you want the masonry to choose a default distance for each pair of subviews.
39 | * `content`: A view builder that creates the content of this masonry.
40 |
41 | ## Masonry
42 | A view that arranges its children in a masonry.
43 |
44 | ### Usage
45 | ```swift
46 | ScrollView(.vertical) {
47 | Masonry(.vertical, lines: 2) {
48 | //Masonry content
49 | }
50 | }
51 | ```
52 |
53 | ### Parameters
54 | * `axis`: The layout axis of this masonry.
55 | * `lines`: The number of lines in the masonry.
56 | * `spacing`: The distance between adjacent subviews, or `nil` if you want the masonry to choose a default distance for each pair of subviews.
57 | * `content`: A view builder that creates the content of this masonry.
58 |
59 | ## Advanced Usage
60 | The distance between adjacent subviews can be controlled in both axes, by using the `horizontalSpacing` and `verticalSpacing` parameters instead of the `spacing` parameter.
61 |
62 | The `lines`, `columns` and `rows` parameters can be initialised with one of the `MasonryLines` cases:
63 | * `adaptive`: A variable number of lines. This case uses the provided `sizeConstraint` to decide the exact number of lines.
64 | * `fixed`: A constant number of lines.
65 |
66 | Masonry views can be initialised with a data source using the `data` and `id` parameters, where `content` builds each child view dynamically from the data source. Using these initialisers also provides access to the `lineSpan`, `columnSpan` and `rowSpan` parameters; return an `Int` or a `MasonryLines` case to set the number of lines a child view will span across.
67 |
68 | The `masonryPlacementMode` view modifier can be used to control how masonry views place their children. Provide a `MasonryPlacementMode` case:
69 | * `fill`: Place each subview in the line with the most available space. This is the default behaviour.
70 | * `order`: Place each subview in view tree order.
71 |
72 | ## Requirements
73 |
74 | * iOS 14.0+, macOS 11.0+, tvOS 14.0+ or watchOS 7.0+
75 | * Xcode 12.0+
76 |
77 | ## Installation
78 |
79 | * Install with [Swift Package Manager](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
80 | * Import `SwiftUIMasonry` to start using.
81 |
82 | ## Contact
83 |
84 | [@ciaranrobrien](https://twitter.com/ciaranrobrien) on Twitter.
85 |
86 |
--------------------------------------------------------------------------------
/Resources/Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciaranrobrien/SwiftUIMasonry/651f84e32edd11919874fbd9167b50ecb60a72db/Resources/Demo.gif
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/API/HMasonry.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | public struct HMasonry: View
10 | where Data : RandomAccessCollection,
11 | ID : Hashable,
12 | Content : View
13 | {
14 | public var body: Masonry
15 | }
16 |
17 |
18 | public extension HMasonry
19 | where Data == [Never],
20 | ID == Never
21 | {
22 | /// A view that arranges its children in a horizontal masonry.
23 | ///
24 | /// This view returns a flexible preferred height to its parent layout.
25 | ///
26 | /// - Parameters:
27 | /// - rows: The number of rows in the masonry.
28 | /// - spacing: The distance between adjacent subviews, or `nil` if you
29 | /// want the masonry to choose a default distance for each pair of
30 | /// subviews.
31 | /// - content: A view builder that creates the content of this masonry.
32 | init(rows: MasonryLines,
33 | spacing: CGFloat? = nil,
34 | @ViewBuilder content: @escaping () -> Content)
35 | {
36 | self.body = Masonry(.horizontal,
37 | lines: rows,
38 | spacing: spacing,
39 | content: content)
40 | }
41 |
42 | /// A view that arranges its children in a horizontal masonry.
43 | ///
44 | /// This view returns a flexible preferred height to its parent layout.
45 | ///
46 | /// - Parameters:
47 | /// - rows: The number of rows in the masonry.
48 | /// - spacing: The distance between adjacent subviews, or `nil` if you
49 | /// want the masonry to choose a default distance for each pair of
50 | /// subviews.
51 | /// - content: A view builder that creates the content of this masonry.
52 | init(rows: Int,
53 | spacing: CGFloat? = nil,
54 | @ViewBuilder content: @escaping () -> Content)
55 | {
56 | self.body = Masonry(.horizontal,
57 | lines: rows,
58 | spacing: spacing,
59 | content: content)
60 | }
61 |
62 | /// A view that arranges its children in a horizontal masonry.
63 | ///
64 | /// This view returns a flexible preferred height to its parent layout.
65 | ///
66 | /// - Parameters:
67 | /// - rows: The number of rows in the masonry.
68 | /// - horizontalSpacing: The distance between horizontally adjacent
69 | /// subviews, or `nil` if you want the masonry to choose a default distance
70 | /// for each pair of subviews.
71 | /// - verticalSpacing: The distance between vertically adjacent
72 | /// subviews, or `nil` if you want the masonry to choose a default distance
73 | /// for each pair of subviews.
74 | /// - content: A view builder that creates the content of this masonry.
75 | init(rows: MasonryLines,
76 | horizontalSpacing: CGFloat? = nil,
77 | verticalSpacing: CGFloat? = nil,
78 | @ViewBuilder content: @escaping () -> Content)
79 | {
80 | self.body = Masonry(.horizontal,
81 | lines: rows,
82 | horizontalSpacing: horizontalSpacing,
83 | verticalSpacing: verticalSpacing,
84 | content: content)
85 | }
86 |
87 | /// A view that arranges its children in a horizontal masonry.
88 | ///
89 | /// This view returns a flexible preferred height to its parent layout.
90 | ///
91 | /// - Parameters:
92 | /// - rows: The number of rows in the masonry.
93 | /// - horizontalSpacing: The distance between horizontally adjacent
94 | /// subviews, or `nil` if you want the masonry to choose a default distance
95 | /// for each pair of subviews.
96 | /// - verticalSpacing: The distance between vertically adjacent
97 | /// subviews, or `nil` if you want the masonry to choose a default distance
98 | /// for each pair of subviews.
99 | /// - content: A view builder that creates the content of this masonry.
100 | init(rows: Int,
101 | horizontalSpacing: CGFloat? = nil,
102 | verticalSpacing: CGFloat? = nil,
103 | @ViewBuilder content: @escaping () -> Content)
104 | {
105 | self.body = Masonry(.horizontal,
106 | lines: rows,
107 | horizontalSpacing: horizontalSpacing,
108 | verticalSpacing: verticalSpacing,
109 | content: content)
110 | }
111 | }
112 |
113 |
114 | public extension HMasonry {
115 |
116 | /// A view that arranges its children in a horizontal masonry.
117 | ///
118 | /// This view returns a flexible preferred height to its parent layout.
119 | ///
120 | /// - Parameters:
121 | /// - rows: The number of rows in the masonry.
122 | /// - spacing: The distance between adjacent subviews, or `nil` if you
123 | /// want the masonry to choose a default distance for each pair of
124 | /// subviews.
125 | /// - data: The data that the masonry uses to create views dynamically.
126 | /// - id: The key path to the provided data's identifier.
127 | /// - content: A view builder that creates the content of this masonry.
128 | /// - rowSpan: The number of rows the content for a given element will
129 | /// span.
130 | init(rows: MasonryLines,
131 | spacing: CGFloat? = nil,
132 | data: Data,
133 | id: KeyPath,
134 | @ViewBuilder content: @escaping (Data.Element) -> Content,
135 | rowSpan: ((Data.Element) -> MasonryLines)? = nil)
136 | {
137 | self.body = Masonry(.horizontal,
138 | lines: rows,
139 | spacing: spacing,
140 | data: data,
141 | id: id,
142 | content: content,
143 | lineSpan: rowSpan)
144 | }
145 |
146 | /// A view that arranges its children in a horizontal masonry.
147 | ///
148 | /// This view returns a flexible preferred height to its parent layout.
149 | ///
150 | /// - Parameters:
151 | /// - rows: The number of rows in the masonry.
152 | /// - spacing: The distance between adjacent subviews, or `nil` if you
153 | /// want the masonry to choose a default distance for each pair of
154 | /// subviews.
155 | /// - data: The data that the masonry uses to create views dynamically.
156 | /// - id: The key path to the provided data's identifier.
157 | /// - content: A view builder that creates the content of this masonry.
158 | /// - rowSpan: The number of rows the content for a given element will
159 | /// span.
160 | init(rows: MasonryLines,
161 | spacing: CGFloat? = nil,
162 | data: Data,
163 | id: KeyPath,
164 | @ViewBuilder content: @escaping (Data.Element) -> Content,
165 | rowSpan: ((Data.Element) -> Int)?)
166 | {
167 | self.body = Masonry(.horizontal,
168 | lines: rows,
169 | spacing: spacing,
170 | data: data,
171 | id: id,
172 | content: content,
173 | lineSpan: rowSpan)
174 | }
175 |
176 | /// A view that arranges its children in a horizontal masonry.
177 | ///
178 | /// This view returns a flexible preferred height to its parent layout.
179 | ///
180 | /// - Parameters:
181 | /// - rows: The number of rows in the masonry.
182 | /// - horizontalSpacing: The distance between horizontally adjacent
183 | /// subviews, or `nil` if you want the masonry to choose a default distance
184 | /// for each pair of subviews.
185 | /// - verticalSpacing: The distance between vertically adjacent
186 | /// subviews, or `nil` if you want the masonry to choose a default distance
187 | /// for each pair of subviews.
188 | /// - data: The data that the masonry uses to create views dynamically.
189 | /// - id: The key path to the provided data's identifier.
190 | /// - content: A view builder that creates the content of this masonry.
191 | /// - rowSpan: The number of rows the content for a given element will
192 | /// span.
193 | init(rows: MasonryLines,
194 | horizontalSpacing: CGFloat? = nil,
195 | verticalSpacing: CGFloat? = nil,
196 | data: Data,
197 | id: KeyPath,
198 | @ViewBuilder content: @escaping (Data.Element) -> Content,
199 | rowSpan: ((Data.Element) -> MasonryLines)? = nil)
200 | {
201 | self.body = Masonry(.horizontal,
202 | lines: rows,
203 | horizontalSpacing: horizontalSpacing,
204 | verticalSpacing: verticalSpacing,
205 | data: data,
206 | id: id,
207 | content: content,
208 | lineSpan: rowSpan)
209 | }
210 |
211 | /// A view that arranges its children in a horizontal masonry.
212 | ///
213 | /// This view returns a flexible preferred height to its parent layout.
214 | ///
215 | /// - Parameters:
216 | /// - rows: The number of rows in the masonry.
217 | /// - horizontalSpacing: The distance between horizontally adjacent
218 | /// subviews, or `nil` if you want the masonry to choose a default distance
219 | /// for each pair of subviews.
220 | /// - verticalSpacing: The distance between vertically adjacent
221 | /// subviews, or `nil` if you want the masonry to choose a default distance
222 | /// for each pair of subviews.
223 | /// - data: The data that the masonry uses to create views dynamically.
224 | /// - id: The key path to the provided data's identifier.
225 | /// - content: A view builder that creates the content of this masonry.
226 | /// - rowSpan: The number of rows the content for a given element will
227 | /// span.
228 | init(rows: MasonryLines,
229 | horizontalSpacing: CGFloat? = nil,
230 | verticalSpacing: CGFloat? = nil,
231 | data: Data,
232 | id: KeyPath,
233 | @ViewBuilder content: @escaping (Data.Element) -> Content,
234 | rowSpan: ((Data.Element) -> Int)?)
235 | {
236 | self.body = Masonry(.horizontal,
237 | lines: rows,
238 | horizontalSpacing: horizontalSpacing,
239 | verticalSpacing: verticalSpacing,
240 | data: data,
241 | id: id,
242 | content: content,
243 | lineSpan: rowSpan)
244 | }
245 | }
246 |
247 |
248 | public extension HMasonry
249 | where Data.Element : Identifiable,
250 | Data.Element.ID == ID
251 | {
252 |
253 | /// A view that arranges its children in a horizontal masonry.
254 | ///
255 | /// This view returns a flexible preferred height to its parent layout.
256 | ///
257 | /// - Parameters:
258 | /// - rows: The number of rows in the masonry.
259 | /// - spacing: The distance between adjacent subviews, or `nil` if you
260 | /// want the masonry to choose a default distance for each pair of
261 | /// subviews.
262 | /// - data: The identified data that the masonry uses to create views
263 | /// dynamically.
264 | /// - content: A view builder that creates the content of this masonry.
265 | /// - rowSpan: The number of rows the content for a given element will
266 | /// span.
267 | init(rows: MasonryLines,
268 | spacing: CGFloat? = nil,
269 | data: Data,
270 | @ViewBuilder content: @escaping (Data.Element) -> Content,
271 | rowSpan: ((Data.Element) -> MasonryLines)? = nil)
272 | {
273 | self.body = Masonry(.horizontal,
274 | lines: rows,
275 | spacing: spacing,
276 | data: data,
277 | content: content,
278 | lineSpan: rowSpan)
279 | }
280 |
281 | /// A view that arranges its children in a horizontal masonry.
282 | ///
283 | /// This view returns a flexible preferred height to its parent layout.
284 | ///
285 | /// - Parameters:
286 | /// - rows: The number of rows in the masonry.
287 | /// - spacing: The distance between adjacent subviews, or `nil` if you
288 | /// want the masonry to choose a default distance for each pair of
289 | /// subviews.
290 | /// - data: The identified data that the masonry uses to create views
291 | /// dynamically.
292 | /// - content: A view builder that creates the content of this masonry.
293 | /// - rowSpan: The number of rows the content for a given element will
294 | /// span.
295 | init(rows: MasonryLines,
296 | spacing: CGFloat? = nil,
297 | data: Data,
298 | @ViewBuilder content: @escaping (Data.Element) -> Content,
299 | rowSpan: ((Data.Element) -> Int)?)
300 | {
301 | self.body = Masonry(.horizontal,
302 | lines: rows,
303 | spacing: spacing,
304 | data: data,
305 | content: content,
306 | lineSpan: rowSpan)
307 | }
308 |
309 | /// A view that arranges its children in a horizontal masonry.
310 | ///
311 | /// This view returns a flexible preferred height to its parent layout.
312 | ///
313 | /// - Parameters:
314 | /// - rows: The number of rows in the masonry.
315 | /// - horizontalSpacing: The distance between horizontally adjacent
316 | /// subviews, or `nil` if you want the masonry to choose a default distance
317 | /// for each pair of subviews.
318 | /// - verticalSpacing: The distance between vertically adjacent
319 | /// subviews, or `nil` if you want the masonry to choose a default distance
320 | /// for each pair of subviews.
321 | /// - data: The identified data that the masonry uses to create views
322 | /// dynamically.
323 | /// - content: A view builder that creates the content of this masonry.
324 | /// - rowSpan: The number of rows the content for a given element will
325 | /// span.
326 | init(rows: MasonryLines,
327 | horizontalSpacing: CGFloat? = nil,
328 | verticalSpacing: CGFloat? = nil,
329 | data: Data,
330 | @ViewBuilder content: @escaping (Data.Element) -> Content,
331 | rowSpan: ((Data.Element) -> MasonryLines)? = nil)
332 | {
333 | self.body = Masonry(.horizontal,
334 | lines: rows,
335 | horizontalSpacing: horizontalSpacing,
336 | verticalSpacing: verticalSpacing,
337 | data: data,
338 | content: content,
339 | lineSpan: rowSpan)
340 | }
341 |
342 | /// A view that arranges its children in a horizontal masonry.
343 | ///
344 | /// This view returns a flexible preferred height to its parent layout.
345 | ///
346 | /// - Parameters:
347 | /// - rows: The number of rows in the masonry.
348 | /// - horizontalSpacing: The distance between horizontally adjacent
349 | /// subviews, or `nil` if you want the masonry to choose a default distance
350 | /// for each pair of subviews.
351 | /// - verticalSpacing: The distance between vertically adjacent
352 | /// subviews, or `nil` if you want the masonry to choose a default distance
353 | /// for each pair of subviews.
354 | /// - data: The identified data that the masonry uses to create views
355 | /// dynamically.
356 | /// - content: A view builder that creates the content of this masonry.
357 | /// - rowSpan: The number of rows the content for a given element will
358 | /// span.
359 | init(rows: MasonryLines,
360 | horizontalSpacing: CGFloat? = nil,
361 | verticalSpacing: CGFloat? = nil,
362 | data: Data,
363 | @ViewBuilder content: @escaping (Data.Element) -> Content,
364 | rowSpan: ((Data.Element) -> Int)?)
365 | {
366 | self.body = Masonry(.horizontal,
367 | lines: rows,
368 | horizontalSpacing: horizontalSpacing,
369 | verticalSpacing: verticalSpacing,
370 | data: data,
371 | content: content,
372 | lineSpan: rowSpan)
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/API/Masonry.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | public struct Masonry: View
10 | where Data : RandomAccessCollection,
11 | ID : Hashable,
12 | Content : View
13 | {
14 | public var body: some View {
15 | GeometryReader { geometry in
16 | MasonryLayout(axis: axis,
17 | content: content,
18 | data: data,
19 | horizontalSpacing: max(horizontalSpacing ?? 8, 0),
20 | id: id,
21 | itemContent: itemContent,
22 | lines: lines,
23 | lineSpan: lineSpan,
24 | size: geometry.size,
25 | verticalSpacing: max(verticalSpacing ?? 8, 0))
26 | .transaction {
27 | updateTransaction($0)
28 | }
29 | .background(
30 | GeometryReader { geometry in
31 | Color.clear
32 | .onAppear { contentSize = geometry.size }
33 | .onChange(of: geometry.size) { newValue in
34 | DispatchQueue.main.async {
35 | withTransaction(transaction) {
36 | contentSize = newValue
37 | }
38 | }
39 | }
40 | }
41 | .hidden()
42 | )
43 | }
44 | .frame(width: axis == .horizontal ? contentSize.width : nil,
45 | height: axis == .vertical ? contentSize.height : nil)
46 | }
47 |
48 | @State private var contentSize = CGSize.zero
49 | @State private var transaction = Transaction()
50 |
51 | private var axis: Axis
52 | private var content: (() -> Content)?
53 | private var data: Data?
54 | private var horizontalSpacing: CGFloat?
55 | private var id: KeyPath?
56 | private var itemContent: ((Data.Element) -> Content)?
57 | private var lines: MasonryLines
58 | private var lineSpan: ((Data.Element) -> MasonryLines)?
59 | private var verticalSpacing: CGFloat?
60 | }
61 |
62 |
63 | public extension Masonry
64 | where Data == [Never],
65 | ID == Never
66 | {
67 | /// A view that arranges its children in a masonry.
68 | ///
69 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
70 | ///
71 | /// - Parameters:
72 | /// - axis: The layout axis of this masonry.
73 | /// - lines: The number of lines in the masonry.
74 | /// - spacing: The distance between adjacent subviews, or `nil` if you
75 | /// want the masonry to choose a default distance for each pair of
76 | /// subviews.
77 | /// - content: A view builder that creates the content of this masonry.
78 | init(_ axis: Axis,
79 | lines: MasonryLines,
80 | spacing: CGFloat? = nil,
81 | @ViewBuilder content: @escaping () -> Content)
82 | {
83 | self.axis = axis
84 | self.content = content
85 | self.data = nil
86 | self.horizontalSpacing = spacing
87 | self.id = nil
88 | self.itemContent = nil
89 | self.lines = lines
90 | self.lineSpan = nil
91 | self.verticalSpacing = spacing
92 | }
93 |
94 | /// A view that arranges its children in a masonry.
95 | ///
96 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
97 | ///
98 | /// - Parameters:
99 | /// - axis: The layout axis of this masonry.
100 | /// - lines: The number of lines in the masonry.
101 | /// - spacing: The distance between adjacent subviews, or `nil` if you
102 | /// want the masonry to choose a default distance for each pair of
103 | /// subviews.
104 | /// - content: A view builder that creates the content of this masonry.
105 | init(_ axis: Axis,
106 | lines: Int,
107 | spacing: CGFloat? = nil,
108 | @ViewBuilder content: @escaping () -> Content)
109 | {
110 | self.axis = axis
111 | self.content = content
112 | self.data = nil
113 | self.horizontalSpacing = spacing
114 | self.id = nil
115 | self.itemContent = nil
116 | self.lines = .fixed(lines)
117 | self.lineSpan = nil
118 | self.verticalSpacing = spacing
119 | }
120 |
121 | /// A view that arranges its children in a masonry.
122 | ///
123 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
124 | ///
125 | /// - Parameters:
126 | /// - axis: The layout axis of this masonry.
127 | /// - lines: The number of lines in the masonry.
128 | /// - horizontalSpacing: The distance between horizontally adjacent
129 | /// subviews, or `nil` if you want the masonry to choose a default distance
130 | /// for each pair of subviews.
131 | /// - verticalSpacing: The distance between vertically adjacent
132 | /// subviews, or `nil` if you want the masonry to choose a default distance
133 | /// for each pair of subviews.
134 | /// - content: A view builder that creates the content of this masonry.
135 | init(_ axis: Axis,
136 | lines: MasonryLines,
137 | horizontalSpacing: CGFloat? = nil,
138 | verticalSpacing: CGFloat? = nil,
139 | @ViewBuilder content: @escaping () -> Content)
140 | {
141 | self.axis = axis
142 | self.content = content
143 | self.data = nil
144 | self.horizontalSpacing = horizontalSpacing
145 | self.id = nil
146 | self.itemContent = nil
147 | self.lines = lines
148 | self.lineSpan = nil
149 | self.verticalSpacing = verticalSpacing
150 | }
151 |
152 | /// A view that arranges its children in a masonry.
153 | ///
154 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
155 | ///
156 | /// - Parameters:
157 | /// - axis: The layout axis of this masonry.
158 | /// - lines: The number of lines in the masonry.
159 | /// - horizontalSpacing: The distance between horizontally adjacent
160 | /// subviews, or `nil` if you want the masonry to choose a default distance
161 | /// for each pair of subviews.
162 | /// - verticalSpacing: The distance between vertically adjacent
163 | /// subviews, or `nil` if you want the masonry to choose a default distance
164 | /// for each pair of subviews.
165 | /// - content: A view builder that creates the content of this masonry.
166 | init(_ axis: Axis,
167 | lines: Int,
168 | horizontalSpacing: CGFloat? = nil,
169 | verticalSpacing: CGFloat? = nil,
170 | @ViewBuilder content: @escaping () -> Content)
171 | {
172 | self.axis = axis
173 | self.content = content
174 | self.data = nil
175 | self.horizontalSpacing = horizontalSpacing
176 | self.id = nil
177 | self.itemContent = nil
178 | self.lines = .fixed(lines)
179 | self.lineSpan = nil
180 | self.verticalSpacing = verticalSpacing
181 | }
182 | }
183 |
184 |
185 | public extension Masonry {
186 |
187 | /// A view that arranges its children in a masonry.
188 | ///
189 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
190 | ///
191 | /// - Parameters:
192 | /// - axis: The layout axis of this masonry.
193 | /// - lines: The number of lines in the masonry.
194 | /// - spacing: The distance between adjacent subviews, or `nil` if you
195 | /// want the masonry to choose a default distance for each pair of
196 | /// subviews.
197 | /// - data: The data that the masonry uses to create views dynamically.
198 | /// - id: The key path to the provided data's identifier.
199 | /// - content: A view builder that creates the content of this masonry.
200 | /// - lineSpan: The number of lines the content for a given element will
201 | /// span.
202 | init(_ axis: Axis,
203 | lines: MasonryLines,
204 | spacing: CGFloat? = nil,
205 | data: Data,
206 | id: KeyPath,
207 | @ViewBuilder content: @escaping (Data.Element) -> Content,
208 | lineSpan: ((Data.Element) -> MasonryLines)? = nil)
209 | {
210 | self.axis = axis
211 | self.content = nil
212 | self.data = data
213 | self.horizontalSpacing = spacing
214 | self.id = id
215 | self.itemContent = content
216 | self.lines = lines
217 | self.lineSpan = lineSpan
218 | self.verticalSpacing = spacing
219 | }
220 |
221 | /// A view that arranges its children in a masonry.
222 | ///
223 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
224 | ///
225 | /// - Parameters:
226 | /// - axis: The layout axis of this masonry.
227 | /// - lines: The number of lines in the masonry.
228 | /// - spacing: The distance between adjacent subviews, or `nil` if you
229 | /// want the masonry to choose a default distance for each pair of
230 | /// subviews.
231 | /// - data: The data that the masonry uses to create views dynamically.
232 | /// - id: The key path to the provided data's identifier.
233 | /// - content: A view builder that creates the content of this masonry.
234 | /// - lineSpan: The number of lines the content for a given element will
235 | /// span.
236 | init(_ axis: Axis,
237 | lines: MasonryLines,
238 | spacing: CGFloat? = nil,
239 | data: Data,
240 | id: KeyPath,
241 | @ViewBuilder content: @escaping (Data.Element) -> Content,
242 | lineSpan: ((Data.Element) -> Int)?)
243 | {
244 | self.axis = axis
245 | self.content = nil
246 | self.data = data
247 | self.horizontalSpacing = spacing
248 | self.id = id
249 | self.itemContent = content
250 | self.lines = lines
251 | self.verticalSpacing = spacing
252 |
253 | if let lineSpan = lineSpan {
254 | self.lineSpan = { .fixed(lineSpan($0)) }
255 | } else {
256 | self.lineSpan = nil
257 | }
258 | }
259 |
260 | /// A view that arranges its children in a masonry.
261 | ///
262 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
263 | ///
264 | /// - Parameters:
265 | /// - axis: The layout axis of this masonry.
266 | /// - lines: The number of lines in the masonry.
267 | /// - horizontalSpacing: The distance between horizontally adjacent
268 | /// subviews, or `nil` if you want the masonry to choose a default distance
269 | /// for each pair of subviews.
270 | /// - verticalSpacing: The distance between vertically adjacent
271 | /// subviews, or `nil` if you want the masonry to choose a default distance
272 | /// for each pair of subviews.
273 | /// - data: The data that the masonry uses to create views dynamically.
274 | /// - id: The key path to the provided data's identifier.
275 | /// - content: A view builder that creates the content of this masonry.
276 | /// - lineSpan: The number of lines the content for a given element will
277 | /// span.
278 | init(_ axis: Axis,
279 | lines: MasonryLines,
280 | horizontalSpacing: CGFloat? = nil,
281 | verticalSpacing: CGFloat? = nil,
282 | data: Data,
283 | id: KeyPath,
284 | @ViewBuilder content: @escaping (Data.Element) -> Content,
285 | lineSpan: ((Data.Element) -> MasonryLines)? = nil)
286 | {
287 | self.axis = axis
288 | self.content = nil
289 | self.data = data
290 | self.horizontalSpacing = horizontalSpacing
291 | self.id = id
292 | self.itemContent = content
293 | self.lines = lines
294 | self.lineSpan = lineSpan
295 | self.verticalSpacing = verticalSpacing
296 | }
297 |
298 | /// A view that arranges its children in a masonry.
299 | ///
300 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
301 | ///
302 | /// - Parameters:
303 | /// - axis: The layout axis of this masonry.
304 | /// - lines: The number of lines in the masonry.
305 | /// - horizontalSpacing: The distance between horizontally adjacent
306 | /// subviews, or `nil` if you want the masonry to choose a default distance
307 | /// for each pair of subviews.
308 | /// - verticalSpacing: The distance between vertically adjacent
309 | /// subviews, or `nil` if you want the masonry to choose a default distance
310 | /// for each pair of subviews.
311 | /// - data: The data that the masonry uses to create views dynamically.
312 | /// - id: The key path to the provided data's identifier.
313 | /// - content: A view builder that creates the content of this masonry.
314 | /// - lineSpan: The number of lines the content for a given element will
315 | /// span.
316 | init(_ axis: Axis,
317 | lines: MasonryLines,
318 | horizontalSpacing: CGFloat? = nil,
319 | verticalSpacing: CGFloat? = nil,
320 | data: Data,
321 | id: KeyPath,
322 | @ViewBuilder content: @escaping (Data.Element) -> Content,
323 | lineSpan: ((Data.Element) -> Int)?)
324 | {
325 | self.axis = axis
326 | self.content = nil
327 | self.data = data
328 | self.horizontalSpacing = horizontalSpacing
329 | self.id = id
330 | self.itemContent = content
331 | self.lines = lines
332 | self.verticalSpacing = verticalSpacing
333 |
334 | if let lineSpan = lineSpan {
335 | self.lineSpan = { .fixed(lineSpan($0)) }
336 | } else {
337 | self.lineSpan = nil
338 | }
339 | }
340 | }
341 |
342 |
343 | public extension Masonry
344 | where Data.Element : Identifiable,
345 | Data.Element.ID == ID
346 | {
347 |
348 | /// A view that arranges its children in a masonry.
349 | ///
350 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
351 | ///
352 | /// - Parameters:
353 | /// - axis: The layout axis of this masonry.
354 | /// - lines: The number of lines in the masonry.
355 | /// - spacing: The distance between adjacent subviews, or `nil` if you
356 | /// want the masonry to choose a default distance for each pair of
357 | /// subviews.
358 | /// - data: The identified data that the masonry uses to create views
359 | /// dynamically.
360 | /// - content: A view builder that creates the content of this masonry.
361 | /// - lineSpan: The number of lines the content for a given element will
362 | /// span.
363 | init(_ axis: Axis,
364 | lines: MasonryLines,
365 | spacing: CGFloat? = nil,
366 | data: Data,
367 | @ViewBuilder content: @escaping (Data.Element) -> Content,
368 | lineSpan: ((Data.Element) -> MasonryLines)? = nil)
369 | {
370 | self.axis = axis
371 | self.content = nil
372 | self.data = data
373 | self.horizontalSpacing = spacing
374 | self.id = \.id
375 | self.itemContent = content
376 | self.lines = lines
377 | self.lineSpan = lineSpan
378 | self.verticalSpacing = spacing
379 | }
380 |
381 | /// A view that arranges its children in a masonry.
382 | ///
383 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
384 | ///
385 | /// - Parameters:
386 | /// - axis: The layout axis of this masonry.
387 | /// - lines: The number of lines in the masonry.
388 | /// - spacing: The distance between adjacent subviews, or `nil` if you
389 | /// want the masonry to choose a default distance for each pair of
390 | /// subviews.
391 | /// - data: The identified data that the masonry uses to create views
392 | /// dynamically.
393 | /// - content: A view builder that creates the content of this masonry.
394 | /// - lineSpan: The number of lines the content for a given element will
395 | /// span.
396 | init(_ axis: Axis,
397 | lines: MasonryLines,
398 | spacing: CGFloat? = nil,
399 | data: Data,
400 | @ViewBuilder content: @escaping (Data.Element) -> Content,
401 | lineSpan: ((Data.Element) -> Int)?)
402 | {
403 | self.axis = axis
404 | self.content = nil
405 | self.data = data
406 | self.horizontalSpacing = spacing
407 | self.id = \.id
408 | self.itemContent = content
409 | self.lines = lines
410 | self.verticalSpacing = spacing
411 |
412 | if let lineSpan = lineSpan {
413 | self.lineSpan = { .fixed(lineSpan($0)) }
414 | } else {
415 | self.lineSpan = nil
416 | }
417 | }
418 |
419 | /// A view that arranges its children in a masonry.
420 | ///
421 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
422 | ///
423 | /// - Parameters:
424 | /// - axis: The layout axis of this masonry.
425 | /// - lines: The number of lines in the masonry.
426 | /// - horizontalSpacing: The distance between horizontally adjacent
427 | /// subviews, or `nil` if you want the masonry to choose a default distance
428 | /// for each pair of subviews.
429 | /// - verticalSpacing: The distance between vertically adjacent
430 | /// subviews, or `nil` if you want the masonry to choose a default distance
431 | /// for each pair of subviews.
432 | /// - data: The identified data that the masonry uses to create views
433 | /// dynamically.
434 | /// - content: A view builder that creates the content of this masonry.
435 | /// - lineSpan: The number of lines the content for a given element will
436 | /// span.
437 | init(_ axis: Axis,
438 | lines: MasonryLines,
439 | horizontalSpacing: CGFloat? = nil,
440 | verticalSpacing: CGFloat? = nil,
441 | data: Data,
442 | @ViewBuilder content: @escaping (Data.Element) -> Content,
443 | lineSpan: ((Data.Element) -> MasonryLines)? = nil)
444 | {
445 | self.axis = axis
446 | self.content = nil
447 | self.data = data
448 | self.horizontalSpacing = horizontalSpacing
449 | self.id = \.id
450 | self.itemContent = content
451 | self.lines = lines
452 | self.lineSpan = lineSpan
453 | self.verticalSpacing = verticalSpacing
454 | }
455 |
456 | /// A view that arranges its children in a masonry.
457 | ///
458 | /// This view returns a flexible preferred size, orthogonal to the layout axis, to its parent layout.
459 | ///
460 | /// - Parameters:
461 | /// - axis: The layout axis of this masonry.
462 | /// - lines: The number of lines in the masonry.
463 | /// - horizontalSpacing: The distance between horizontally adjacent
464 | /// subviews, or `nil` if you want the masonry to choose a default distance
465 | /// for each pair of subviews.
466 | /// - verticalSpacing: The distance between vertically adjacent
467 | /// subviews, or `nil` if you want the masonry to choose a default distance
468 | /// for each pair of subviews.
469 | /// - data: The identified data that the masonry uses to create views
470 | /// dynamically.
471 | /// - content: A view builder that creates the content of this masonry.
472 | /// - lineSpan: The number of lines the content for a given element will
473 | /// span.
474 | init(_ axis: Axis,
475 | lines: MasonryLines,
476 | horizontalSpacing: CGFloat? = nil,
477 | verticalSpacing: CGFloat? = nil,
478 | data: Data,
479 | @ViewBuilder content: @escaping (Data.Element) -> Content,
480 | lineSpan: ((Data.Element) -> Int)?)
481 | {
482 | self.axis = axis
483 | self.content = nil
484 | self.data = data
485 | self.horizontalSpacing = horizontalSpacing
486 | self.id = \.id
487 | self.itemContent = content
488 | self.lines = lines
489 | self.verticalSpacing = verticalSpacing
490 |
491 | if let lineSpan = lineSpan {
492 | self.lineSpan = { .fixed(lineSpan($0)) }
493 | } else {
494 | self.lineSpan = nil
495 | }
496 | }
497 | }
498 |
499 |
500 | private extension Masonry {
501 | func updateTransaction(_ newValue: Transaction) {
502 | if transaction.animation != newValue.animation
503 | || transaction.disablesAnimations != newValue.disablesAnimations
504 | || transaction.isContinuous != newValue.isContinuous
505 | {
506 | DispatchQueue.main.async {
507 | transaction = newValue
508 | }
509 | }
510 | }
511 | }
512 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/API/MasonryLines.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// Constants that define the number of lines in a masonry view.
10 | public enum MasonryLines {
11 |
12 | /// A variable number of lines.
13 | ///
14 | /// This case uses the provided `sizeConstraint` to decide the exact
15 | /// number of lines.
16 | case adaptive(sizeConstraint: AdaptiveSizeConstraint)
17 |
18 | /// A constant number of lines.
19 | case fixed(Int)
20 | }
21 |
22 |
23 | public extension MasonryLines {
24 |
25 | /// A variable number of lines.
26 | ///
27 | /// This case uses the provided `minSize` to decide the exact number of
28 | /// lines.
29 | static func adaptive(minSize: CGFloat) -> MasonryLines {
30 | .adaptive(sizeConstraint: .min(minSize))
31 | }
32 |
33 | /// A variable number of lines.
34 | ///
35 | /// This case uses the provided `maxSize` to decide the exact number of
36 | /// lines.
37 | static func adaptive(maxSize: CGFloat) -> MasonryLines {
38 | .adaptive(sizeConstraint: .max(maxSize))
39 | }
40 | }
41 |
42 |
43 | public extension MasonryLines {
44 | /// Constants that constrain the bounds of an adaptive line in a masonry
45 | /// view.
46 | enum AdaptiveSizeConstraint: Equatable {
47 |
48 | /// The minimum size of a line in a given axis.
49 | case min(CGFloat)
50 |
51 | /// The maximum size of a line in a given axis.
52 | case max(CGFloat)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/API/MasonryPlacementMode.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | /// Constants that define how a masonry's subviews are placed in the available
10 | /// space.
11 | public enum MasonryPlacementMode: Hashable, CaseIterable {
12 |
13 | /// Place each subview in the line with the most available space.
14 | case fill
15 |
16 | /// Place each subview in view tree order.
17 | case order
18 | }
19 |
20 |
21 | public extension View {
22 |
23 | /// Sets the placement mode for masonries within this view.
24 | func masonryPlacementMode(_ mode: MasonryPlacementMode) -> some View {
25 | environment(\.masonryPlacementMode, mode)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/API/VMasonry.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | public struct VMasonry: View
10 | where Data : RandomAccessCollection,
11 | ID : Hashable,
12 | Content : View
13 | {
14 | public var body: Masonry
15 | }
16 |
17 |
18 | public extension VMasonry
19 | where Data == [Never],
20 | ID == Never
21 | {
22 | /// A view that arranges its children in a vertical masonry.
23 | ///
24 | /// This view returns a flexible preferred width to its parent layout.
25 | ///
26 | /// - Parameters:
27 | /// - columns: The number of columns in the masonry.
28 | /// - spacing: The distance between adjacent subviews, or `nil` if you
29 | /// want the masonry to choose a default distance for each pair of
30 | /// subviews.
31 | /// - content: A view builder that creates the content of this masonry.
32 | init(columns: MasonryLines,
33 | spacing: CGFloat? = nil,
34 | @ViewBuilder content: @escaping () -> Content)
35 | {
36 | self.body = Masonry(.vertical,
37 | lines: columns,
38 | spacing: spacing,
39 | content: content)
40 | }
41 |
42 | /// A view that arranges its children in a vertical masonry.
43 | ///
44 | /// This view returns a flexible preferred width to its parent layout.
45 | ///
46 | /// - Parameters:
47 | /// - columns: The number of columns in the masonry.
48 | /// - spacing: The distance between adjacent subviews, or `nil` if you
49 | /// want the masonry to choose a default distance for each pair of
50 | /// subviews.
51 | /// - content: A view builder that creates the content of this masonry.
52 | init(columns: Int,
53 | spacing: CGFloat? = nil,
54 | @ViewBuilder content: @escaping () -> Content)
55 | {
56 | self.body = Masonry(.vertical,
57 | lines: columns,
58 | spacing: spacing,
59 | content: content)
60 | }
61 |
62 | /// A view that arranges its children in a vertical masonry.
63 | ///
64 | /// This view returns a flexible preferred width to its parent layout.
65 | ///
66 | /// - Parameters:
67 | /// - columns: The number of columns in the masonry.
68 | /// - horizontalSpacing: The distance between horizontally adjacent
69 | /// subviews, or `nil` if you want the masonry to choose a default distance
70 | /// for each pair of subviews.
71 | /// - verticalSpacing: The distance between vertically adjacent
72 | /// subviews, or `nil` if you want the masonry to choose a default distance
73 | /// for each pair of subviews.
74 | /// - content: A view builder that creates the content of this masonry.
75 | init(columns: MasonryLines,
76 | horizontalSpacing: CGFloat? = nil,
77 | verticalSpacing: CGFloat? = nil,
78 | @ViewBuilder content: @escaping () -> Content)
79 | {
80 | self.body = Masonry(.vertical,
81 | lines: columns,
82 | horizontalSpacing: horizontalSpacing,
83 | verticalSpacing: verticalSpacing,
84 | content: content)
85 | }
86 |
87 | /// A view that arranges its children in a vertical masonry.
88 | ///
89 | /// This view returns a flexible preferred width to its parent layout.
90 | ///
91 | /// - Parameters:
92 | /// - columns: The number of columns in the masonry.
93 | /// - horizontalSpacing: The distance between horizontally adjacent
94 | /// subviews, or `nil` if you want the masonry to choose a default distance
95 | /// for each pair of subviews.
96 | /// - verticalSpacing: The distance between vertically adjacent
97 | /// subviews, or `nil` if you want the masonry to choose a default distance
98 | /// for each pair of subviews.
99 | /// - content: A view builder that creates the content of this masonry.
100 | init(columns: Int,
101 | horizontalSpacing: CGFloat? = nil,
102 | verticalSpacing: CGFloat? = nil,
103 | @ViewBuilder content: @escaping () -> Content)
104 | {
105 | self.body = Masonry(.vertical,
106 | lines: columns,
107 | horizontalSpacing: horizontalSpacing,
108 | verticalSpacing: verticalSpacing,
109 | content: content)
110 | }
111 | }
112 |
113 |
114 | public extension VMasonry {
115 |
116 | /// A view that arranges its children in a vertical masonry.
117 | ///
118 | /// This view returns a flexible preferred width to its parent layout.
119 | ///
120 | /// - Parameters:
121 | /// - columns: The number of columns in the masonry.
122 | /// - spacing: The distance between adjacent subviews, or `nil` if you
123 | /// want the masonry to choose a default distance for each pair of
124 | /// subviews.
125 | /// - data: The data that the masonry uses to create views dynamically.
126 | /// - id: The key path to the provided data's identifier.
127 | /// - content: A view builder that creates the content of this masonry.
128 | /// - columnSpan: The number of columns the content for a given element
129 | /// will span.
130 | init(columns: MasonryLines,
131 | spacing: CGFloat? = nil,
132 | data: Data,
133 | id: KeyPath,
134 | @ViewBuilder content: @escaping (Data.Element) -> Content,
135 | columnSpan: ((Data.Element) -> MasonryLines)? = nil)
136 | {
137 | self.body = Masonry(.vertical,
138 | lines: columns,
139 | spacing: spacing,
140 | data: data,
141 | id: id,
142 | content: content,
143 | lineSpan: columnSpan)
144 | }
145 |
146 | /// A view that arranges its children in a vertical masonry.
147 | ///
148 | /// This view returns a flexible preferred width to its parent layout.
149 | ///
150 | /// - Parameters:
151 | /// - columns: The number of columns in the masonry.
152 | /// - spacing: The distance between adjacent subviews, or `nil` if you
153 | /// want the masonry to choose a default distance for each pair of
154 | /// subviews.
155 | /// - data: The data that the masonry uses to create views dynamically.
156 | /// - id: The key path to the provided data's identifier.
157 | /// - content: A view builder that creates the content of this masonry.
158 | /// - columnSpan: The number of columns the content for a given element
159 | /// will span.
160 | init(columns: MasonryLines,
161 | spacing: CGFloat? = nil,
162 | data: Data,
163 | id: KeyPath,
164 | @ViewBuilder content: @escaping (Data.Element) -> Content,
165 | columnSpan: ((Data.Element) -> Int)?)
166 | {
167 | self.body = Masonry(.vertical,
168 | lines: columns,
169 | spacing: spacing,
170 | data: data,
171 | id: id,
172 | content: content,
173 | lineSpan: columnSpan)
174 | }
175 |
176 | /// A view that arranges its children in a vertical masonry.
177 | ///
178 | /// This view returns a flexible preferred width to its parent layout.
179 | ///
180 | /// - Parameters:
181 | /// - columns: The number of columns in the masonry.
182 | /// - horizontalSpacing: The distance between horizontally adjacent
183 | /// subviews, or `nil` if you want the masonry to choose a default distance
184 | /// for each pair of subviews.
185 | /// - verticalSpacing: The distance between vertically adjacent
186 | /// subviews, or `nil` if you want the masonry to choose a default distance
187 | /// for each pair of subviews.
188 | /// - data: The data that the masonry uses to create views dynamically.
189 | /// - id: The key path to the provided data's identifier.
190 | /// - content: A view builder that creates the content of this masonry.
191 | /// - columnSpan: The number of columns the content for a given element
192 | /// will span.
193 | init(columns: MasonryLines,
194 | horizontalSpacing: CGFloat? = nil,
195 | verticalSpacing: CGFloat? = nil,
196 | data: Data,
197 | id: KeyPath,
198 | @ViewBuilder content: @escaping (Data.Element) -> Content,
199 | columnSpan: ((Data.Element) -> MasonryLines)? = nil)
200 | {
201 | self.body = Masonry(.vertical,
202 | lines: columns,
203 | horizontalSpacing: horizontalSpacing,
204 | verticalSpacing: verticalSpacing,
205 | data: data,
206 | id: id,
207 | content: content,
208 | lineSpan: columnSpan)
209 | }
210 |
211 | /// A view that arranges its children in a vertical masonry.
212 | ///
213 | /// This view returns a flexible preferred width to its parent layout.
214 | ///
215 | /// - Parameters:
216 | /// - columns: The number of columns in the masonry.
217 | /// - horizontalSpacing: The distance between horizontally adjacent
218 | /// subviews, or `nil` if you want the masonry to choose a default distance
219 | /// for each pair of subviews.
220 | /// - verticalSpacing: The distance between vertically adjacent
221 | /// subviews, or `nil` if you want the masonry to choose a default distance
222 | /// for each pair of subviews.
223 | /// - data: The data that the masonry uses to create views dynamically.
224 | /// - id: The key path to the provided data's identifier.
225 | /// - content: A view builder that creates the content of this masonry.
226 | /// - columnSpan: The number of columns the content for a given element
227 | /// will span.
228 | init(columns: MasonryLines,
229 | horizontalSpacing: CGFloat? = nil,
230 | verticalSpacing: CGFloat? = nil,
231 | data: Data,
232 | id: KeyPath,
233 | @ViewBuilder content: @escaping (Data.Element) -> Content,
234 | columnSpan: ((Data.Element) -> Int)?)
235 | {
236 | self.body = Masonry(.vertical,
237 | lines: columns,
238 | horizontalSpacing: horizontalSpacing,
239 | verticalSpacing: verticalSpacing,
240 | data: data,
241 | id: id,
242 | content: content,
243 | lineSpan: columnSpan)
244 | }
245 | }
246 |
247 |
248 | public extension VMasonry
249 | where Data.Element : Identifiable,
250 | Data.Element.ID == ID
251 | {
252 |
253 | /// A view that arranges its children in a vertical masonry.
254 | ///
255 | /// This view returns a flexible preferred width to its parent layout.
256 | ///
257 | /// - Parameters:
258 | /// - columns: The number of columns in the masonry.
259 | /// - spacing: The distance between adjacent subviews, or `nil` if you
260 | /// want the masonry to choose a default distance for each pair of
261 | /// subviews.
262 | /// - data: The identified data that the masonry uses to create views
263 | /// dynamically.
264 | /// - content: A view builder that creates the content of this masonry.
265 | /// - columnSpan: The number of columns the content for a given element
266 | /// will span.
267 | init(columns: MasonryLines,
268 | spacing: CGFloat? = nil,
269 | data: Data,
270 | @ViewBuilder content: @escaping (Data.Element) -> Content,
271 | columnSpan: ((Data.Element) -> MasonryLines)? = nil)
272 | {
273 | self.body = Masonry(.vertical,
274 | lines: columns,
275 | spacing: spacing,
276 | data: data,
277 | content: content,
278 | lineSpan: columnSpan)
279 | }
280 |
281 | /// A view that arranges its children in a vertical masonry.
282 | ///
283 | /// This view returns a flexible preferred width to its parent layout.
284 | ///
285 | /// - Parameters:
286 | /// - columns: The number of columns in the masonry.
287 | /// - spacing: The distance between adjacent subviews, or `nil` if you
288 | /// want the masonry to choose a default distance for each pair of
289 | /// subviews.
290 | /// - data: The identified data that the masonry uses to create views
291 | /// dynamically.
292 | /// - content: A view builder that creates the content of this masonry.
293 | /// - columnSpan: The number of columns the content for a given element
294 | /// will span.
295 | init(columns: MasonryLines,
296 | spacing: CGFloat? = nil,
297 | data: Data,
298 | @ViewBuilder content: @escaping (Data.Element) -> Content,
299 | columnSpan: ((Data.Element) -> Int)?)
300 | {
301 | self.body = Masonry(.vertical,
302 | lines: columns,
303 | spacing: spacing,
304 | data: data,
305 | content: content,
306 | lineSpan: columnSpan)
307 | }
308 |
309 | /// A view that arranges its children in a vertical masonry.
310 | ///
311 | /// This view returns a flexible preferred width to its parent layout.
312 | ///
313 | /// - Parameters:
314 | /// - columns: The number of columns in the masonry.
315 | /// - horizontalSpacing: The distance between horizontally adjacent
316 | /// subviews, or `nil` if you want the masonry to choose a default distance
317 | /// for each pair of subviews.
318 | /// - verticalSpacing: The distance between vertically adjacent
319 | /// subviews, or `nil` if you want the masonry to choose a default distance
320 | /// for each pair of subviews.
321 | /// - data: The identified data that the masonry uses to create views
322 | /// dynamically.
323 | /// - content: A view builder that creates the content of this masonry.
324 | /// - columnSpan: The number of columns the content for a given element
325 | /// will span.
326 | init(columns: MasonryLines,
327 | horizontalSpacing: CGFloat? = nil,
328 | verticalSpacing: CGFloat? = nil,
329 | data: Data,
330 | @ViewBuilder content: @escaping (Data.Element) -> Content,
331 | columnSpan: ((Data.Element) -> MasonryLines)? = nil)
332 | {
333 | self.body = Masonry(.vertical,
334 | lines: columns,
335 | horizontalSpacing: horizontalSpacing,
336 | verticalSpacing: verticalSpacing,
337 | data: data,
338 | content: content,
339 | lineSpan: columnSpan)
340 | }
341 |
342 | /// A view that arranges its children in a vertical masonry.
343 | ///
344 | /// This view returns a flexible preferred width to its parent layout.
345 | ///
346 | /// - Parameters:
347 | /// - columns: The number of columns in the masonry.
348 | /// - horizontalSpacing: The distance between horizontally adjacent
349 | /// subviews, or `nil` if you want the masonry to choose a default distance
350 | /// for each pair of subviews.
351 | /// - verticalSpacing: The distance between vertically adjacent
352 | /// subviews, or `nil` if you want the masonry to choose a default distance
353 | /// for each pair of subviews.
354 | /// - data: The identified data that the masonry uses to create views
355 | /// dynamically.
356 | /// - content: A view builder that creates the content of this masonry.
357 | /// - columnSpan: The number of columns the content for a given element
358 | /// will span.
359 | init(columns: MasonryLines,
360 | horizontalSpacing: CGFloat? = nil,
361 | verticalSpacing: CGFloat? = nil,
362 | data: Data,
363 | @ViewBuilder content: @escaping (Data.Element) -> Content,
364 | columnSpan: ((Data.Element) -> Int)?)
365 | {
366 | self.body = Masonry(.vertical,
367 | lines: columns,
368 | horizontalSpacing: horizontalSpacing,
369 | verticalSpacing: verticalSpacing,
370 | data: data,
371 | content: content,
372 | lineSpan: columnSpan)
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/Internal/EnvironmentValues+.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal extension EnvironmentValues {
10 | var masonryPlacementMode: MasonryPlacementMode {
11 | get { self[MasonryPlacementModeKey.self] }
12 | set { self[MasonryPlacementModeKey.self] = newValue }
13 | }
14 | }
15 |
16 |
17 | private struct MasonryPlacementModeKey: EnvironmentKey {
18 | static let defaultValue = MasonryPlacementMode.fill
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/Internal/MasonryDynamicViewBuilder.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct MasonryDynamicViewBuilder: View
10 | where Data : RandomAccessCollection,
11 | ID : Hashable,
12 | Content : View
13 | {
14 | var axis: Axis
15 | var content: (Data.Element) -> Content
16 | var data: Data
17 | var horizontalSpacing: CGFloat
18 | var id: KeyPath
19 | var lineCount: Int
20 | var lineSize: CGFloat
21 | var lineSpan: ((Data.Element) -> MasonryLines)?
22 | var verticalSpacing: CGFloat
23 |
24 | var body: some View {
25 | ForEach(data, id: id) { element in
26 | let size = itemSize(element)
27 |
28 | content(element)
29 | .frame(width: axis == .vertical ? size : nil, height: axis == .horizontal ? size : nil)
30 | }
31 | }
32 |
33 | private var currentSpacing: CGFloat {
34 | switch axis {
35 | case .horizontal: return verticalSpacing
36 | case .vertical: return horizontalSpacing
37 | }
38 | }
39 |
40 | private func itemSize(_ element: Data.Element) -> CGFloat {
41 | guard lineSize > 0
42 | else { return 0 }
43 |
44 | let elementLines = lineSpan?(element) ?? .fixed(1)
45 | var lineSpan: Int
46 |
47 | switch elementLines {
48 | case .adaptive(let sizeConstraint):
49 | switch sizeConstraint {
50 | case .min(let size):
51 | let value = floor(size / lineSize)
52 | lineSpan = Int(value.isFinite ? value : 0)
53 |
54 | case .max(let size):
55 | let value = ceil(size / lineSize)
56 | lineSpan = Int(value.isFinite ? value : 0)
57 | }
58 |
59 | case .fixed(let count):
60 | lineSpan = count
61 | }
62 |
63 | return ((lineSize + currentSpacing) * CGFloat(min(max(lineSpan, 1), lineCount))) - currentSpacing
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/SwiftUIMasonry/Internal/MasonryLayout.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * SwiftUIMasonry
3 | * Copyright (c) Ciaran O'Brien 2022
4 | * MIT license, see LICENSE file for details
5 | */
6 |
7 | import SwiftUI
8 |
9 | internal struct MasonryLayout: View
10 | where Data : RandomAccessCollection,
11 | ID : Hashable,
12 | Content : View
13 | {
14 | @Environment(\.masonryPlacementMode) private var placementMode
15 |
16 | var axis: Axis
17 | var content: (() -> Content)?
18 | var data: Data?
19 | var horizontalSpacing: CGFloat
20 | var id: KeyPath?
21 | var itemContent: ((Data.Element) -> Content)?
22 | var lines: MasonryLines
23 | var lineSpan: ((Data.Element) -> MasonryLines)?
24 | var size: CGSize
25 | var verticalSpacing: CGFloat
26 |
27 | var body: some View {
28 | var alignments = Array(repeating: CGFloat.zero, count: lineCount)
29 | var currentIndex = defaultIndex
30 | var currentLineSpan = 1
31 | var top: CGFloat = 0
32 |
33 | ZStack(alignment: .topLeading) {
34 | Group {
35 | if let content = content {
36 | content()
37 | .frame(width: axis == .vertical ? lineSize : nil, height: axis == .horizontal ? lineSize : nil)
38 | } else if let data = data, let id = id, let itemContent = itemContent {
39 | MasonryDynamicViewBuilder(axis: axis,
40 | content: itemContent,
41 | data: data,
42 | horizontalSpacing: horizontalSpacing,
43 | id: id,
44 | lineCount: lineCount,
45 | lineSize: lineSize,
46 | lineSpan: lineSpan,
47 | verticalSpacing: verticalSpacing)
48 | }
49 | }
50 | .fixedSize(horizontal: axis == .horizontal, vertical: axis == .vertical)
51 | .alignmentGuide(.leading) { dimensions in
52 | func updateCurrentLineSpan() {
53 | switch axis {
54 | case .horizontal:
55 | currentLineSpan = Int(round((dimensions.height + verticalSpacing) / (lineSize + verticalSpacing)))
56 | case .vertical:
57 | currentLineSpan = Int(round((dimensions.width + horizontalSpacing) / (lineSize + horizontalSpacing)))
58 | }
59 | }
60 |
61 | switch placementMode {
62 | case .fill:
63 | updateCurrentLineSpan()
64 |
65 | currentIndex = alignments
66 | .enumerated()
67 | .map { enumerated -> (element: CGFloat, offset: Int) in
68 | let element = (0.. $1.element }
76 | .first!
77 | .offset
78 |
79 | case .order:
80 | currentIndex += currentLineSpan
81 |
82 | updateCurrentLineSpan()
83 |
84 | if currentIndex + currentLineSpan > lineCount {
85 | currentIndex = 0
86 | }
87 | }
88 |
89 | switch axis {
90 | case .horizontal:
91 | let leading = alignments[currentIndex..