Layout, stacks and adaptive UI
Compose screens with VStack, HStack and ZStack, build grids, read available space safely, and make one layout work from iPhone SE to iPad with Dynamic Type.
Stacks, frames and alignment
struct MetricCard: View {
let title: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
Text(value)
.font(.title2.bold())
.monospacedDigit()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
}
}
struct Dashboard: View {
var body: some View {
HStack(alignment: .top, spacing: 12) {
MetricCard(title: "Open", value: "12")
MetricCard(title: "Closed", value: "903")
}
.padding(.horizontal)
}
}frame(maxWidth: .infinity)makes a child fill the space a stack offers; without it a stack hugs its content.- Order matters: padding before background paints the background behind the padded area, after it paints inside.
ZStacklayers views; usealignment:on the stack rather than offsets where possible.- Prefer
Spacer()andDivider()over hard-coded pixel gaps so text scaling does not break the layout.
Grids and available space
struct PhotoGrid: View {
let photos: [Photo]
private let columns = [
GridItem(.adaptive(minimum: 140), spacing: 8)
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(photos) { photo in
Image(photo.name)
.resizable()
.scaledToFill()
.frame(minHeight: 140)
.clipped()
}
}
.padding(8)
}
}
}| Need | Prefer | Avoid |
|---|---|---|
| Equal columns | LazyVGrid(.flexible()) | GeometryReader math |
| Rows of tags | Layout protocol or ViewThatFits | Manual width arithmetic |
| One measured dimension | GeometryReader inside a background | Wrapping the whole screen |
| Sidebar on iPad | NavigationSplitView | Two separate screens |
GeometryReader is greedy: it expands to the space offered and reports it through the closure. Put it inside a .background when you only need a measurement, so it does not change your real layout.
Safe areas, size classes and text scaling
struct ReaderView: View {
@Environment(\.horizontalSizeClass) private var sizeClass
var body: some View {
ScrollView {
Text(bodyText)
.font(.body)
.dynamicTypeSize(...DynamicTypeSize.accessibility3)
.padding(.horizontal, sizeClass == .regular ? 48 : 16)
}
.safeAreaInset(edge: .bottom) {
Button("Next") { advance() }
.frame(maxWidth: .infinity)
.padding()
.background(.bar)
}
}
}💡
Test every screen at the smallest text size and at
accessibility5. Fixed-height rows and single-line labels are the two things that break first, and both are trivially reproducible in the Xcode preview canvas.FAQ
When should I use <code>NavigationSplitView</code>?
Whenever the content benefits from a persistent sidebar on a regular-width device. It collapses to a stack automatically in compact width, so you write one layout instead of branching on device type.
Why does my text get clipped with larger accessibility sizes?
A fixed
frame(height:) or a single-line label cannot grow. Remove the height constraint or set lineLimit(nil) and let the row size itself.Related
Lists, forms and data-driven views Localization, accessibility and system integration
Last refreshed 2026-09-18.