How to use AnyLayout in SwiftUI | SwiftUI Bootcamp #70
Building an app that looks great on both iPhone and iPad used to mean duplicating your view code inside if/else branches — one for VStack, one for HStack. AnyLayout eliminates that duplication by letting you swap the layout container itself while keeping the child views written only once.
What You'll Learn
- What
AnyLayoutis and how it type-erases layout containers likeVStackLayoutandHStackLayout - How to read
horizontalSizeClassandverticalSizeClassto adapt layout to the current device - Why
AnyLayoutpreserves view identity across layout transitions, enabling smooth animations
Mental Model
Think of AnyLayout as a universal adapter plug. Your child views (Text("Alpha"), etc.) are the appliances — they don't change. The layout container (VStackLayout vs HStackLayout) is the socket — it determines how the appliances are arranged. AnyLayout is the adapter that lets you plug any socket into the same wall, swapping sockets at runtime without rewiring the appliances.
Because the children maintain identity through the layout swap, SwiftUI can animate the transition between vertical and horizontal arrangements — the views glide to their new positions rather than abruptly appearing there.
Detailed Explanation
AnyLayout is a type-erasing wrapper for any type that conforms to the Layout protocol. It was introduced in iOS 16 alongside the Layout protocol itself. Without AnyLayout, you cannot store a VStackLayout or HStackLayout in a variable because they are different concrete types. AnyLayout gives you a single type to hold either.
VStackLayout, HStackLayout, ZStackLayout, and GridLayout are the layout equivalents of VStack, HStack, ZStack, and Grid — but they're standalone values, not view containers. You wrap them in AnyLayout(...) and then call that AnyLayout value as a function (using the callAsFunction mechanism) with a @ViewBuilder closure, just like you'd call VStack { ... }.
The @Environment(\.horizontalSizeClass) and @Environment(\.verticalSizeClass) values are how SwiftUI exposes the current device size class. On iPhone in portrait, horizontalSizeClass is .compact. On iPad and iPhone in landscape, it may be .regular. These values change dynamically, so your layout updates automatically when the user rotates their device or resizes a Split View on iPad.
Minimum deployment target: iOS 16 is required for AnyLayout. For iOS 15 and earlier, the if/else duplication pattern is the only option.
Code Structure
AnyLayoutBootcamp.swift contains one view that reads both size class values from the environment, constructs an AnyLayout based on horizontalSizeClass, and uses it to lay out three text views. The commented-out if/else block immediately below shows the pre-AnyLayout alternative — making the reduction in code duplication easy to appreciate.
Complete Code
AnyLayoutBootcamp.swift
import SwiftUI
// https://useyourloaf.com/blog/size-classes/
struct AnyLayoutBootcamp: View {
// Reads the horizontal size class from the environment — updates automatically on rotation/resize
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
var body: some View {
VStack(spacing: 12) {
Text("Horizontal: \(horizontalSizeClass.debugDescription)") // Useful for debugging in simulator
Text("Vertical: \(verticalSizeClass.debugDescription)")
// Build the layout value based on size class — compact = vertical, regular = horizontal
let layout: AnyLayout = horizontalSizeClass == .compact ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
// Call layout like a function — children are written once, layout adapts
layout {
Text("Alpha")
Text("Beta")
Text("Gamma")
}
// if horizontalSizeClass == .compact {
// VStack {
// Text("Alpha")
// Text("Beta")
// Text("Gamma")
// }
// } else {
// HStack {
// Text("Alpha")
// Text("Beta")
// Text("Gamma")
// }
// }
}
}
}
struct AnyLayoutBootcamp_Previews: PreviewProvider {
static var previews: some View {
AnyLayoutBootcamp()
}
}Code Walkthrough
@Environment(\.horizontalSizeClass)— This reads the current horizontal size class from SwiftUI's environment. It's an optionalUserInterfaceSizeClass— it can be.compact,.regular, ornil(in some Mac Catalyst contexts). The view re-renders automatically when this value changes.@Environment(\.verticalSizeClass)— The vertical counterpart. It's less commonly used for layout decisions but useful for adapting to landscape on iPhone (where vertical size class becomes.compact).let layout: AnyLayout = ...— The layout is computed as a local constant insidebody. Sincebodyis recomputed whenever state or environment changes, this always reflects the current size class. The ternary picksVStackLayoutfor compact (iPhone portrait) andHStackLayoutfor regular (iPad, landscape).layout { Text("Alpha") ... }—AnyLayoutconforms toLayout, which includes acallAsFunction(@ViewBuilder content:). This syntax lets you call the layout as if it were a function, passing your child views in a trailing closure. The children are written once — not duplicated.Commented-out
if/else— This is the pre-iOS 16 approach. The child views are duplicated — any change to the content (adding a fourth text, changing styling) must be made in both branches.AnyLayouteliminates this maintenance burden.
Common Mistakes
Mistake: Forgetting that AnyLayout requires iOS 16+AnyLayout and VStackLayout/HStackLayout are iOS 16 APIs. If your minimum deployment target is iOS 15, this pattern won't compile. Use the if/else approach for iOS 15 support, or raise your deployment target.
Mistake: Using AnyLayout for simple two-branch layouts when an if/else is clearerAnyLayout shines when you have several child views and the layout is the only thing changing. For a single view that moves position, a conditional alignment or offset is simpler. Use AnyLayout when child view duplication is the problem you're solving.
Mistake: Not reading the linked size class reference
The code links to https://useyourloaf.com/blog/size-classes/ for a reason — size class combinations are not always obvious. iPhone in landscape is .compact/.compact, iPad is .regular/.regular, iPad in split view can be .compact/.regular. Understanding the combinations helps you design accurate layout conditions.
Key Takeaways
AnyLayouttype-erases layout containers so you can store and swap them as a variable — eliminating child view duplication in adaptive layouts- Read
@Environment(\.horizontalSizeClass)and@Environment(\.verticalSizeClass)to make layout decisions — SwiftUI updates them automatically on rotation and resize - Because child views maintain identity through layout transitions, SwiftUI can animate the shift from
VStacktoHStackarrangement smoothly
Last updated: June 27, 2026