ViewThatFits: Adaptive Layouts in SwiftUI
Goal
By the end of this tutorial, you will build a small adaptive row that picks the best layout for available space using ViewThatFits.
State it explicitly:
“Render a rich horizontal row on wide space, and automatically fall back to compact variants on smaller widths.”
Requirements
- Basic SwiftUI knowledge
- Xcode 15+
- iOS 16+ (
ViewThatFitsis available from iOS 16)
The use case
A common UI problem appears in cards, list rows, and toolbar areas: the same content must stay readable in multiple widths. Developers often solve this by measuring geometry manually or branching with device checks. That works, but those solutions grow brittle quickly.
ViewThatFits gives you a simpler approach.
You declare multiple view variants in priority order.
SwiftUI keeps the first one that fits in the current axis and ignores the rest.
Step 1 - The simplest working version
Start with one fallback chain for width:
import SwiftUI
struct ProductMetaRow: View {
let title: String
let price: String
let rating: String
var body: some View {
ViewThatFits(in: .horizontal) {
HStack(spacing: 8) {
Text(title).font(.headline)
Spacer(minLength: 4)
Text(price).font(.subheadline)
Text(rating).font(.caption)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Capsule().fill(.yellow.opacity(0.2)))
}
HStack(spacing: 8) {
Text(title).font(.headline).lineLimit(1)
Spacer(minLength: 4)
Text(price).font(.subheadline)
}
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline).lineLimit(2)
Text(price).font(.subheadline)
}
}
}
}
What this already does correctly:
ViewThatFits tries the first layout, then the second, then the third, and picks the first variant that fits horizontally.
Quick check: Put this row inside previews with narrow and wide frames and verify the selected variant changes automatically.
Step 2 - Make it reusable
The first version works, but the fallback options are still tied to one concrete row. Now extract a reusable adaptive container.
struct AdaptiveHorizontal<Primary: View, Secondary: View, Compact: View>: View {
@ViewBuilder let primary: () -> Primary
@ViewBuilder let secondary: () -> Secondary
@ViewBuilder let compact: () -> Compact
var body: some View {
ViewThatFits(in: .horizontal) {
primary()
secondary()
compact()
}
}
}
Use it inside your row:
struct ProductMetaRow: View {
let title: String
let price: String
let rating: String
var body: some View {
AdaptiveHorizontal {
HStack(spacing: 8) {
Text(title).font(.headline)
Spacer(minLength: 4)
Text(price).font(.subheadline)
Text(rating).font(.caption)
}
} secondary: {
HStack(spacing: 8) {
Text(title).font(.headline).lineLimit(1)
Spacer(minLength: 4)
Text(price).font(.subheadline)
}
} compact: {
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline).lineLimit(2)
Text(price).font(.subheadline)
}
}
}
}
What changed: The fallback strategy is now a reusable pattern instead of one-off inline layout code.
Quick check:
Re-use AdaptiveHorizontal in a second component (for example, a profile row) and confirm behavior stays consistent.
Step 3 - SwiftUI-specific refinement
Now add polish that keeps transitions and truncation predictable:
struct ProductMetaRow: View {
let title: String
let price: String
let rating: String
var body: some View {
AdaptiveHorizontal {
HStack(spacing: 8) {
Text(title)
.font(.headline)
.lineLimit(1)
Spacer(minLength: 4)
Text(price).font(.subheadline)
Label(rating, systemImage: "star.fill")
.labelStyle(.titleAndIcon)
.font(.caption)
.foregroundStyle(.orange)
}
} secondary: {
HStack(spacing: 8) {
Text(title)
.font(.headline)
.lineLimit(1)
.layoutPriority(1)
Spacer(minLength: 4)
Text(price).font(.subheadline)
}
} compact: {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.lineLimit(2)
Text(price)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.animation(.default, value: title)
}
}
Refinement points:
layoutPriority helps the right text stay visible longer in intermediate widths, and lineLimit makes truncation deterministic.
You still avoid manual size checks.
Quick check: Try long titles in preview and confirm fallback order remains readable.
Result
You now have an adaptive layout pattern that is:
- declarative
- reusable
- easier to maintain than geometry-branch logic
Common SwiftUI pitfalls
-
Mistake: Using
ViewThatFitswith variants that are nearly identical
Why it happens: fallback chain has no meaningful progression
How to fix it: make each variant intentionally more compact -
Mistake: Relying on device type instead of space
Why it happens: old UIKit-style adaptation habits
How to fix it: adapt by available width, not by device name -
Mistake: No truncation rules in fallback variants
Why it happens: assuming fitting picks perfect text behavior
How to fix it: setlineLimit,layoutPriority, and spacing intentionally
When NOT to use this
If your layout needs exact size-aware calculations (for example, custom charts or pixel-precise alignment), GeometryReader or a custom Layout may be more appropriate.
ViewThatFits is best when you have discrete fallback variants.
Takeaway
If you remember one thing:
Use ViewThatFits when adaptation can be expressed as ordered layout variants, not manual measurement logic.
Next steps
If you want to connect adaptive layout with broader SwiftUI architecture, continue with:
- How to Structure a SwiftUI View File (A Beginner’s Guide)
- Extracting a reusable SwiftUI component cleanly
- SwiftUI Is a Language for Data Flow (Not Layout)
External inspiration: