Skip to content

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 AnyLayout is and how it type-erases layout containers like VStackLayout and HStackLayout
  • How to read horizontalSizeClass and verticalSizeClass to adapt layout to the current device
  • Why AnyLayout preserves 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

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

  1. @Environment(\.horizontalSizeClass) — This reads the current horizontal size class from SwiftUI's environment. It's an optional UserInterfaceSizeClass — it can be .compact, .regular, or nil (in some Mac Catalyst contexts). The view re-renders automatically when this value changes.

  2. @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).

  3. let layout: AnyLayout = ... — The layout is computed as a local constant inside body. Since body is recomputed whenever state or environment changes, this always reflects the current size class. The ternary picks VStackLayout for compact (iPhone portrait) and HStackLayout for regular (iPad, landscape).

  4. layout { Text("Alpha") ... }AnyLayout conforms to Layout, which includes a callAsFunction(@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.

  5. 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. AnyLayout eliminates 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 clearer
AnyLayout 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

  • AnyLayout type-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 VStack to HStack arrangement smoothly

Last updated: June 27, 2026

Released under the MIT License.