├── .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 | ![Demo](./Resources/Demo.gif "Demo") 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..