Created
April 24, 2025 14:37
-
-
Save sebj/194e85934bfd771fc45c542e29f8a833 to your computer and use it in GitHub Desktop.
SwiftUI Custom 'Masonry' Layout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
/// A layout that places views horizontally until the maximum width is filled, and allows views to flow onto new rows. | |
/// Each row is horizontally centered in the available width. | |
/// This layout assumes all views have the same height. | |
struct MasonryLayout: Layout { | |
let spacing: CGFloat | |
func makeCache(subviews: Subviews) -> Cache { | |
.init() | |
} | |
func sizeThatFits( | |
proposal: ProposedViewSize, | |
subviews: Subviews, | |
cache: inout Cache | |
) -> CGSize { | |
guard !subviews.isEmpty else { | |
return .zero | |
} | |
if cache.width > 0 && cache.height > 0 { | |
return .init(width: cache.width, height: cache.height) | |
} | |
cacheDimensionsAndOrigins(proposal: proposal, subviews: subviews, cache: &cache) | |
return .init(width: cache.width, height: cache.height) | |
} | |
func placeSubviews( | |
in bounds: CGRect, | |
proposal: ProposedViewSize, | |
subviews: Subviews, | |
cache: inout Cache | |
) { | |
guard !cache.rows.isEmpty else { | |
return | |
} | |
var lastIndex = 0 | |
for row in cache.rows { | |
let rowXOffset = (bounds.size.width - row.width) / 2 | |
for origin in row.origins { | |
let subview = subviews[lastIndex] | |
subview.place( | |
at: .init(x: origin.x + bounds.origin.x + rowXOffset, y: origin.y + bounds.origin.y), | |
anchor: .topLeading, | |
proposal: .unspecified | |
) | |
lastIndex += 1 | |
} | |
} | |
} | |
private func cacheDimensionsAndOrigins( | |
proposal: ProposedViewSize, | |
subviews: Subviews, | |
cache: inout Cache | |
) { | |
let proposedWidth = proposal.width ?? 0 | |
var y: Double = 0 | |
var lastViewHeight: Double = 0 | |
var partialRowOrigins: [CGPoint] = [] | |
var partialRowWidth = 0.0 | |
var rows: [Cache.Row] = [] | |
subviews.forEach { subview in | |
let dimensions = subview.dimensions(in: .unspecified) | |
if partialRowWidth + spacing + dimensions.width > proposedWidth { | |
rows.append(.init(origins: partialRowOrigins, width: partialRowWidth)) | |
partialRowOrigins = [] | |
partialRowWidth = 0 | |
y += spacing + dimensions.height | |
} | |
if partialRowWidth > 0 { | |
partialRowWidth += spacing | |
} | |
partialRowOrigins.append(.init(x: partialRowWidth, y: y)) | |
partialRowWidth += dimensions.width | |
lastViewHeight = dimensions.height | |
} | |
if partialRowWidth > 0 && !partialRowOrigins.isEmpty { | |
rows.append(.init(origins: partialRowOrigins, width: partialRowWidth)) | |
} | |
cache.width = proposedWidth | |
cache.height = y + lastViewHeight | |
cache.rows = rows | |
} | |
struct Cache { | |
fileprivate var width: Double = 0 | |
fileprivate var height: Double = 0 | |
fileprivate var rows: [Row] = [] | |
struct Row { | |
let origins: [CGPoint] | |
let width: Double | |
} | |
init() {} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment