Skip to content

Instantly share code, notes, and snippets.

@trenskow
Created April 14, 2025 16:11
Show Gist options
  • Save trenskow/25f29969d41ec15e0e19a7e9efbff8eb to your computer and use it in GitHub Desktop.
Save trenskow/25f29969d41ec15e0e19a7e9efbff8eb to your computer and use it in GitHub Desktop.
Flow layout SwiftUI
import SwiftUI
struct Flow: Layout {
struct PartialRect {
let subview: LayoutSubview
let x: CGFloat
let size: CGSize
}
typealias Cache = [[CGRect]]
let lineAlignment: Alignment
let spacing: CGSize
init(
lineAlignment alignment: Alignment = .topLeading,
spacing: CGSize
) {
self.lineAlignment = alignment
self.spacing = spacing
}
func makeCache(
subviews: Subviews
) -> Cache {
return Cache()
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
cache = []
let width = proposal.width ?? subviews
.reduce(0) { max($0, $1.sizeThatFits(proposal).width) }
var offsetX: CGFloat = 0
var offsetY: CGFloat = 0
var currentLine: [PartialRect] = []
for subview in subviews {
let subviewWidth = min(width, subview.sizeThatFits(proposal).width)
if offsetX + subviewWidth > width {
offsetY += self.commit(
proposal: proposal,
line: currentLine,
offsetY: offsetY,
width: width,
cache: &cache)
offsetX = 0
currentLine = []
}
let viewWidth = min(width, subview.sizeThatFits(proposal).width)
let viewHeight = subview.sizeThatFits(proposal).height
currentLine.append(
PartialRect(
subview: subview,
x: offsetX,
size: CGSize(
width: viewWidth,
height: viewHeight)))
offsetX += viewWidth + self.spacing.width
}
if !currentLine.isEmpty {
self.commit(
proposal: proposal,
line: currentLine,
offsetY: offsetY,
width: width,
cache: &cache)
}
return CGSize(
width: cache.reduce([], +)
.reduce(0) { width, line in
return max(width, line.maxX)
},
height: cache.reduce([], +)
.reduce(0) { height, line in
return max(height, line.maxY)
})
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
let allCache = cache.reduce([], +)
for idx in 0..<subviews.count {
let subview = subviews[idx]
let rect = allCache[idx]
subview.place(
at: CGPoint(
x: bounds.origin.x + rect.origin.x,
y: bounds.origin.y + rect.origin.y),
proposal: .init(
rect.size))
}
}
@discardableResult
private func commit(
proposal: ProposedViewSize,
line: [PartialRect],
offsetY: CGFloat,
width: CGFloat,
cache: inout Cache
) -> CGFloat {
let height = line
.reduce(0) { max($0, $1.size.height) }
let diffX = width - line
.reduce(0) { max($0, $1.x + $1.size.width) }
var offsetX: CGFloat
switch self.lineAlignment.horizontal {
case .trailing, .listRowSeparatorTrailing:
offsetX = diffX
case .center:
offsetX = diffX / 2
default:
offsetX = 0
}
var cacheLine: [CGRect] = []
for partialRect in line {
let lineDiffY = height - partialRect.size.height
var lineOffsetY: CGFloat
switch self.lineAlignment.vertical {
case .bottom:
lineOffsetY = lineDiffY
case .center:
lineOffsetY = lineDiffY / 2
default:
lineOffsetY = 0
}
cacheLine.append(
CGRect(
x: partialRect.x + offsetX,
y: offsetY + lineOffsetY,
width: partialRect.size.width,
height: partialRect.size.height))
}
cache.append(
cacheLine)
return height + self.spacing.height
}
}
extension Flow {
init(
lineAlignment alignment: Alignment = .topLeading,
spacingHorizontal: CGFloat = 0,
spacingVertical: CGFloat = 0
) {
self.init(
lineAlignment: alignment,
spacing: CGSize(
width: spacingHorizontal,
height: spacingVertical))
}
init(
lineAlignment alignment: Alignment = .topLeading,
spacing: CGFloat
) {
self.init(
lineAlignment: alignment,
spacing: CGSize(
width: spacing,
height: spacing))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment