The Layout protocol (iOS 16) opens the layout system without GeometryReader: implement sizeThatFits to report your size and placeSubviews to position children. The worked example is a flow layout, an HStack that wraps to a new line when the width runs out.
Measure in sizeThatFits
Ask each subview for its ideal size with sizeThatFits(.unspecified) (.zero and .infinity return the minimum and maximum instead). Walk the sizes, wrap when a line exceeds the proposed width, and return the total.
struct FlowLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
var totalHeight: CGFloat = 0
var totalWidth: CGFloat = 0
var lineWidth: CGFloat = 0
var lineHeight: CGFloat = 0
for size in sizes {
if lineWidth + size.width > proposal.width ?? 0 {
totalHeight += lineHeight
lineWidth = size.width
lineHeight = size.height
} else {
lineWidth += size.width
lineHeight = max(lineHeight, size.height)
}
totalWidth = max(totalWidth, lineWidth)
}
totalHeight += lineHeight
return .init(width: totalWidth, height: totalHeight)
}
}
Place in placeSubviews
The bounds rectangle is where your layout lives, and its origin is not zero: always start from bounds.minX and bounds.minY, never from the origin. Place each child through the Subviews proxy.
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
var lineX = bounds.minX
var lineY = bounds.minY
var lineHeight: CGFloat = 0
for index in subviews.indices {
if lineX + sizes[index].width > (proposal.width ?? 0) {
lineY += lineHeight
lineHeight = 0
lineX = bounds.minX
}
subviews[index].place(
at: .init(x: lineX + sizes[index].width / 2, y: lineY + sizes[index].height / 2),
anchor: .center,
proposal: ProposedViewSize(sizes[index])
)
lineHeight = max(lineHeight, sizes[index].height)
lineX += sizes[index].width
}
}
Use It Like a Stack
A Layout type is called like a container view.
FlowLayout {
ForEach(tags) { tag in
TagView(tag: tag)
}
}
Available on iOS 16, macOS 13, tvOS 16, and watchOS 9. Follow-up docs cover caching, custom spacing, and LayoutValueKey.