Skip to content

Instantly share code, notes, and snippets.

@sebj
Created April 24, 2025 14:37
Show Gist options
  • Save sebj/194e85934bfd771fc45c542e29f8a833 to your computer and use it in GitHub Desktop.
Save sebj/194e85934bfd771fc45c542e29f8a833 to your computer and use it in GitHub Desktop.
SwiftUI Custom 'Masonry' Layout
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