A custom Layout is a plain struct, so spacing is just a property. The deeper technique: a nil spacing should fall back to SwiftUI's platform-preferred spacing, which the Subview proxy can compute per pair of views.
Fixed Spacing Is a Property
Add spacing to the layout and include it in both the size calculation and the placement advance.
struct FlowLayout: Layout {
var spacing: CGFloat = 0
}
// sizeThatFits, when the view stays on the line
lineWidth += cache.sizes[index].width + spacing
// placeSubviews, after placing a view
lineX += cache.sizes[index].width + spacing
// call site
FlowLayout(spacing: 8) { /* views */ }
Preferred Spacing From the Subview Proxy
SwiftUI knows the preferred gap between view kinds (Image next to Text differs from Text next to Text), and the values differ per platform. subview.spacing.distance(to:along:) returns that preferred gap for a pair of neighbors on a given axis.
Cache the pair distances in makeCache: they only change when the subviews change.
struct FlowLayout: Layout {
var spacing: CGFloat? = nil
struct Cache {
var sizes: [CGSize] = []
var spacing: [CGFloat] = []
}
func makeCache(subviews: Subviews) -> Cache {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
let spacing: [CGFloat] = subviews.indices.map { index in
guard index != subviews.count - 1 else { return 0 }
return subviews[index].spacing.distance(
to: subviews[index + 1].spacing,
along: .horizontal
)
}
return Cache(sizes: sizes, spacing: spacing)
}
}
nil Means Preferred, Not Zero
Match the built-in stacks: an explicit value wins, otherwise use the cached preferred distance.
// in both sizeThatFits and placeSubviews
lineX += cache.sizes[index].width + (spacing ?? cache.spacing[index])
With that, FlowLayout { ... } without arguments spaces its children the way an HStack would on the current platform.