swiftui · tutorial · layout

ViewThatFits: Adaptive Layouts in SwiftUI

Published March 11, 2026 · 4 min read · intermediate

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+ (ViewThatFits is 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 ViewThatFits with 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: set lineLimit, 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:

External inspiration: