- 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:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
How to Change the Background Color of a View in SwiftUI
Learn different ways to set background colors on SwiftUI views, from simple color fills to gradients and materials.
Rounding Corners in SwiftUI: From cornerRadius to clipShape
The cornerRadius modifier is deprecated in iOS 17. Here's how to use clipShape, RoundedRectangle, and the new ConcentricRectangle to round corners the modern way.
How to Round Specific Corners of a View in SwiftUI
Learn how to round only certain corners of a SwiftUI view using UnevenRoundedRectangle, custom shapes, and clipShape.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.