Beyond VStack & HStack: Custom Layouts in SwiftUI
Learn how to build flexible, reusable layouts with Layout and ViewThatFits instead of reaching for Spacer().
SwiftUI gives us VStack, HStack, and ZStack out of the box—but when you need more control, the layout system has a lot more to offer. Here’s a quick example using ViewThatFits to switch between horizontal and vertical layouts based on available space.
ViewThatFits: Let the system choose
Instead of hard-coding a layout, you can offer two (or more) options and let SwiftUI pick the first one that fits:
struct AdaptiveLabel: View {
let icon: String
let title: String
var body: some View {
ViewThatFits(in: .horizontal) {
HStack(spacing: 8) {
Image(systemName: icon)
Text(title)
}
VStack(alignment: .leading, spacing: 4) {
Image(systemName: icon)
Text(title)
}
}
}
}
In narrow widths (e.g. compact size classes or small windows), the view will use the VStack; when there’s enough horizontal space, it uses the HStack. No Spacer() or GeometryReader required.
images/ folder and reference them here.Custom Layout with the Layout protocol
For full control, implement the Layout protocol (iOS 16+). You provide sizeThatFits and placeSubviews:
struct MyCustomLayout: Layout {
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
// Compute and return the layout size
subviews.reduce(CGSize.zero) { size, subview in
let subviewSize = subview.sizeThatFits(.unspecified)
return CGSize(
width: max(size.width, subviewSize.width),
height: size.height + subviewSize.height
)
}
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
var y = bounds.minY
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
subview.place(at: CGPoint(x: bounds.minX, y: y), proposal: .unspecified)
y += size.height
}
}
}
Once you have a custom layout, use it like any other layout container: MyCustomLayout() { ... }. This is a great way to build reusable layout components that adapt to your design system.
If you want to go deeper, I cover these ideas (and how to avoid Spacer()) in my video Stop Using Spacer() in SwiftUI. Happy layout hacking!