BS
BleepingSwift
Published on
4 min read

> ViewThatFits for Adaptive Layouts in SwiftUI

Building layouts that adapt to different screen sizes usually means checking size classes or geometry and conditionally showing different views. ViewThatFits simplifies this by automatically selecting the first child view that fits in the available space.

Basic Usage

List your layout options from most preferred to least preferred. SwiftUI tries each one and uses the first that fits:

struct AdaptiveLabel: View {
    var body: some View {
        ViewThatFits {
            Label("Welcome to the App", systemImage: "star.fill")
                .font(.title)

            Label("Welcome", systemImage: "star.fill")
                .font(.title)

            Image(systemName: "star.fill")
                .font(.title)
        }
    }
}

In a wide container, the full label appears. As space shrinks, it switches to the shorter text, then just the icon. You don't need to calculate breakpoints yourself.

How It Works

ViewThatFits measures each child's ideal size and picks the first one that fits within the proposed size. The ideal size is what a view would occupy with unlimited space, before any compression.

This means the order matters. Put your preferred (usually larger) layouts first:

ViewThatFits {
    HStack {
        Image(systemName: "photo")
        Text("View Full Gallery")
        Spacer()
        Image(systemName: "chevron.right")
    }

    HStack {
        Image(systemName: "photo")
        Text("Gallery")
    }

    Image(systemName: "photo")
}

Constraining to One Axis

By default, ViewThatFits checks both horizontal and vertical space. You can constrain it to a single axis:

struct ScrollableText: View {
    let content: String

    var body: some View {
        ViewThatFits(in: .vertical) {
            Text(content)

            ScrollView {
                Text(content)
            }
        }
    }
}

This checks only vertical space. If the text fits without scrolling, it shows plain text. If it's too tall, it wraps in a ScrollView. The horizontal axis is ignored for the fit calculation, so text can still wrap to multiple lines.

Horizontal Layout Switching

A common pattern is switching between horizontal and vertical arrangements:

struct ActionButtons: View {
    var body: some View {
        ViewThatFits {
            HStack(spacing: 16) {
                Button("Cancel") { }
                    .buttonStyle(.bordered)
                Button("Save") { }
                    .buttonStyle(.borderedProminent)
            }

            VStack(spacing: 12) {
                Button("Save") { }
                    .buttonStyle(.borderedProminent)
                Button("Cancel") { }
                    .buttonStyle(.bordered)
            }
        }
        .padding()
    }
}

On wider screens, buttons sit side by side. On narrow screens, they stack vertically.

With Dynamic Type

ViewThatFits works well with Dynamic Type. As text size increases, layouts automatically adapt:

struct ProfileHeader: View {
    let name: String
    let title: String

    var body: some View {
        ViewThatFits {
            HStack(spacing: 16) {
                avatar
                VStack(alignment: .leading) {
                    Text(name).font(.headline)
                    Text(title).font(.subheadline)
                }
            }

            VStack {
                avatar
                Text(name).font(.headline)
                Text(title).font(.subheadline)
            }
        }
    }

    var avatar: some View {
        Circle()
            .fill(.blue)
            .frame(width: 50, height: 50)
    }
}

At normal text sizes, the name appears beside the avatar. With larger accessibility sizes, it stacks vertically.

Avoiding Common Mistakes

Don't use flexible views as alternatives. Views with Spacer() or .frame(maxWidth: .infinity) will always claim to fit because they can shrink to any size:

// This won't work as expected
ViewThatFits {
    HStack {
        Text("Option A")
        Spacer()  // Makes this view always "fit"
    }

    Text("Option B")  // Never selected
}

Instead, use fixed-size alternatives:

ViewThatFits {
    HStack {
        Text("Option A")
        Text("Extra Info")
    }

    Text("Option A")
}

Practical Example

Here's a toolbar that adapts its layout based on available width:

struct AdaptiveToolbar: View {
    var body: some View {
        ViewThatFits {
            HStack(spacing: 20) {
                toolbarButton(icon: "square.and.arrow.up", label: "Share")
                toolbarButton(icon: "doc.on.doc", label: "Copy")
                toolbarButton(icon: "trash", label: "Delete")
                toolbarButton(icon: "pencil", label: "Edit")
            }

            HStack(spacing: 16) {
                toolbarButton(icon: "square.and.arrow.up", label: nil)
                toolbarButton(icon: "doc.on.doc", label: nil)
                toolbarButton(icon: "trash", label: nil)
                toolbarButton(icon: "pencil", label: nil)
            }
        }
        .padding()
        .background(.bar)
    }

    func toolbarButton(icon: String, label: String?) -> some View {
        Button {
            // action
        } label: {
            if let label {
                Label(label, systemImage: icon)
            } else {
                Image(systemName: icon)
            }
        }
    }
}

When there's room, buttons show icons with labels. When space is tight, only icons appear.

When to Use ViewThatFits

Use it when you have discrete layout alternatives and want SwiftUI to pick the best one. It's ideal for adaptive text labels, switching between horizontal and vertical layouts, and showing or hiding optional content based on space.

For more complex responsive layouts where you need to know the exact available size, GeometryReader or the Layout protocol might be better choices. But for most adaptive layout needs, ViewThatFits handles the job with far less code.

Sample Project

Want to see this code in action? Check out the complete sample project on GitHub:

View on GitHub

The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.