axiom-swiftui-layout-ref
Reference — Complete SwiftUI adaptive layout API guide covering ViewThatFits, AnyLayout, Layout protocol, onGeometryChange, GeometryReader, size classes, and iOS 26 window APIs
Best use case
axiom-swiftui-layout-ref is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Reference — Complete SwiftUI adaptive layout API guide covering ViewThatFits, AnyLayout, Layout protocol, onGeometryChange, GeometryReader, size classes, and iOS 26 window APIs
Teams using axiom-swiftui-layout-ref should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/axiom-swiftui-layout-ref/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How axiom-swiftui-layout-ref Compares
| Feature / Agent | axiom-swiftui-layout-ref | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Reference — Complete SwiftUI adaptive layout API guide covering ViewThatFits, AnyLayout, Layout protocol, onGeometryChange, GeometryReader, size classes, and iOS 26 window APIs
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# SwiftUI Layout API Reference
Comprehensive API reference for SwiftUI adaptive layout tools. For decision guidance and anti-patterns, see the `axiom-swiftui-layout` skill.
## Overview
This reference covers all SwiftUI layout APIs for building adaptive interfaces:
- **ViewThatFits** — Automatic variant selection (iOS 16+)
- **AnyLayout** — Type-erased animated layout switching (iOS 16+)
- **Layout Protocol** — Custom layout algorithms (iOS 16+)
- **onGeometryChange** — Efficient geometry reading (iOS 16+ backported)
- **GeometryReader** — Layout-phase geometry access (iOS 13+)
- **Safe Area Padding** — .safeAreaPadding() vs .padding() (iOS 17+)
- **Size Classes** — Trait-based adaptation
- **iOS 26 Window APIs** — Free-form windows, menu bar, resize anchors
---
## ViewThatFits
Evaluates child views in order and displays the first one that fits in the available space.
### Basic Usage
```swift
ViewThatFits {
// First choice
HStack {
icon
title
Spacer()
button
}
// Second choice
HStack {
icon
title
button
}
// Fallback
VStack {
HStack { icon; title }
button
}
}
```
### With Axis Constraint
```swift
// Only consider horizontal fit
ViewThatFits(in: .horizontal) {
wideVersion
narrowVersion
}
// Only consider vertical fit
ViewThatFits(in: .vertical) {
tallVersion
shortVersion
}
```
### How It Works
1. Applies `fixedSize()` to each child
2. Measures ideal size against available space
3. Returns first child that fits
4. Falls back to last child if none fit
### Limitations
- Does not expose which variant was selected
- Cannot animate between variants (use AnyLayout instead)
- Measures all variants (performance consideration for complex views)
---
## AnyLayout
Type-erased layout container enabling animated transitions between layouts.
### Basic Usage
```swift
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var sizeClass
var layout: AnyLayout {
sizeClass == .compact
? AnyLayout(VStackLayout(spacing: 12))
: AnyLayout(HStackLayout(spacing: 20))
}
var body: some View {
layout {
ForEach(items) { item in
ItemView(item: item)
}
}
.animation(.default, value: sizeClass)
}
}
```
### Available Layout Types
```swift
AnyLayout(HStackLayout(alignment: .top, spacing: 10))
AnyLayout(VStackLayout(alignment: .leading, spacing: 8))
AnyLayout(ZStackLayout(alignment: .center))
AnyLayout(GridLayout(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10))
```
### Custom Conditions
```swift
// Based on Dynamic Type
@Environment(\.dynamicTypeSize) var typeSize
var layout: AnyLayout {
typeSize.isAccessibilitySize
? AnyLayout(VStackLayout())
: AnyLayout(HStackLayout())
}
// Based on geometry
@State private var isWide = true
var layout: AnyLayout {
isWide
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
}
```
### Why Use Over Conditional Views
```swift
// ❌ Loses view identity, no animation
if isCompact {
VStack { content }
} else {
HStack { content }
}
// ✅ Preserves identity, smooth animation
let layout = isCompact ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
layout { content }
```
---
## Layout Protocol
Create custom layout containers with full control over positioning.
### Basic Custom Layout
```swift
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
return calculateSize(for: sizes, in: proposal.width ?? .infinity)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var point = bounds.origin
var lineHeight: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if point.x + size.width > bounds.maxX {
point.x = bounds.origin.x
point.y += lineHeight + spacing
lineHeight = 0
}
subview.place(at: point, proposal: .unspecified)
point.x += size.width + spacing
lineHeight = max(lineHeight, size.height)
}
}
}
// Usage
FlowLayout(spacing: 12) {
ForEach(tags) { tag in
TagView(tag: tag)
}
}
```
### With Cache
```swift
struct CachedLayout: Layout {
struct CacheData {
var sizes: [CGSize] = []
}
func makeCache(subviews: Subviews) -> CacheData {
CacheData(sizes: subviews.map { $0.sizeThatFits(.unspecified) })
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
// Use cache.sizes instead of measuring again
}
}
```
### Layout Values
```swift
// Define custom layout value
struct Rank: LayoutValueKey {
static let defaultValue: Int = 0
}
extension View {
func rank(_ value: Int) -> some View {
layoutValue(key: Rank.self, value: value)
}
}
// Read in layout
func placeSubviews(...) {
let sorted = subviews.sorted { $0[Rank.self] < $1[Rank.self] }
}
```
---
## onGeometryChange
Efficient geometry reading without layout side effects. Backported to iOS 16+.
### Basic Usage
```swift
@State private var size: CGSize = .zero
var body: some View {
content
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
size = newSize
}
}
```
### Reading Specific Values
```swift
// Width only
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { width in
columnCount = max(1, Int(width / 150))
}
// Frame in coordinate space
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { frame in
globalFrame = frame
}
// Aspect ratio
.onGeometryChange(for: Bool.self) { proxy in
proxy.size.width > proxy.size.height
} action: { isWide in
self.isWide = isWide
}
```
### Coordinate Spaces
```swift
// Named coordinate space
ScrollView {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .named("scroll")).minY
} action: { offset in
scrollOffset = offset
}
}
.coordinateSpace(name: "scroll")
```
### Comparison with GeometryReader
| Aspect | onGeometryChange | GeometryReader |
|--------|------------------|----------------|
| Layout impact | None | Greedy (fills space) |
| When evaluated | After layout | During layout |
| Use case | Side effects | Layout calculations |
| iOS version | 16+ (backported) | 13+ |
---
## GeometryReader
Provides geometry information during layout phase. Use sparingly due to greedy sizing.
### Basic Usage (Constrained)
```swift
// ✅ Always constrain GeometryReader
GeometryReader { proxy in
let width = proxy.size.width
HStack(spacing: 0) {
Rectangle().frame(width: width * 0.3)
Rectangle().frame(width: width * 0.7)
}
}
.frame(height: 100) // Required constraint
```
### GeometryProxy Properties
```swift
GeometryReader { proxy in
// Container size
let size = proxy.size // CGSize
// Safe area insets
let insets = proxy.safeAreaInsets // EdgeInsets
// Frame in coordinate space
let globalFrame = proxy.frame(in: .global)
let localFrame = proxy.frame(in: .local)
let namedFrame = proxy.frame(in: .named("container"))
}
```
### Common Patterns
```swift
// Proportional sizing
GeometryReader { geo in
VStack {
header.frame(height: geo.size.height * 0.2)
content.frame(height: geo.size.height * 0.8)
}
}
// Centering with offset
GeometryReader { geo in
content
.position(x: geo.size.width / 2, y: geo.size.height / 2)
}
```
### Avoiding Common Mistakes
```swift
// ❌ Unconstrained in VStack
VStack {
GeometryReader { ... } // Takes ALL space
Button("Next") { } // Invisible
}
// ✅ Constrained
VStack {
GeometryReader { ... }
.frame(height: 200)
Button("Next") { }
}
// ❌ Causing layout loops
GeometryReader { geo in
content
.frame(width: geo.size.width) // Can cause infinite loop
}
```
---
## Safe Area Padding
SwiftUI provides two primary approaches for handling spacing around content: `.padding()` and `.safeAreaPadding()`. Understanding when to use each is critical for proper layout on devices with safe areas (notch, Dynamic Island, home indicator).
### The Critical Difference
```swift
// ❌ WRONG - Ignores safe areas, content hits notch/home indicator
ScrollView {
content
}
.padding(.horizontal, 20)
// ✅ CORRECT - Respects safe areas, adds padding beyond them
ScrollView {
content
}
.safeAreaPadding(.horizontal, 20)
```
**Key insight**: `.padding()` adds fixed spacing from the view's edges. `.safeAreaPadding()` adds spacing beyond the safe area insets.
### When to Use Each
#### Use `.padding()` when
- Adding spacing between sibling views within a container
- Creating internal spacing that should be consistent everywhere
- Working with views that already respect safe areas (like List, Form)
- Adding decorative spacing on macOS (no safe area concerns)
```swift
VStack(spacing: 0) {
header
.padding(.horizontal, 16) // ✅ Internal spacing
Divider()
content
.padding(.horizontal, 16) // ✅ Internal spacing
}
```
#### Use `.safeAreaPadding()` when (iOS 17+)
- Adding margin to full-width content that extends to screen edges
- Implementing edge-to-edge scrolling with proper insets
- Creating custom containers that need safe area awareness
- Working with Liquid Glass or full-screen materials
```swift
// ✅ Edge-to-edge list with custom padding
List(items) { item in
ItemRow(item)
}
.listStyle(.plain)
.safeAreaPadding(.horizontal, 20) // Adds 20pt beyond safe areas
// ✅ Full-screen content with proper margins
ZStack {
Color.blue.ignoresSafeArea()
VStack {
content
}
.safeAreaPadding(.all, 16) // Respects notch, home indicator
}
```
### Platform Availability
**iOS 17+, iPadOS 17+, macOS 14+, axiom-visionOS 1.0+**
For earlier iOS versions, use manual safe area handling:
```swift
// iOS 13-16 fallback
GeometryReader { geo in
content
.padding(.horizontal, 20 + geo.safeAreaInsets.leading)
}
```
Or conditional compilation:
```swift
if #available(iOS 17, *) {
content.safeAreaPadding(.horizontal, 20)
} else {
content.padding(.horizontal, 20)
.padding(.leading, safeAreaInsets.leading)
}
```
### Edge-Specific Usage
```swift
// Top only (below status bar/notch)
.safeAreaPadding(.top, 8)
// Bottom only (above home indicator)
.safeAreaPadding(.bottom, 16)
// Horizontal (left/right of safe areas)
.safeAreaPadding(.horizontal, 20)
// All edges
.safeAreaPadding(.all, 16)
// Individual edges
.safeAreaPadding(EdgeInsets(top: 8, leading: 20, bottom: 16, trailing: 20))
```
### Common Patterns
#### Edge-to-Edge ScrollView
```swift
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemCard(item)
}
}
}
.safeAreaPadding(.horizontal, 16) // Content inset from edges + safe areas
.safeAreaPadding(.vertical, 8)
```
#### Full-Screen Background with Safe Content
```swift
ZStack {
// Background extends edge-to-edge
LinearGradient(...)
.ignoresSafeArea()
// Content respects safe areas + custom padding
VStack {
header
Spacer()
content
Spacer()
footer
}
.safeAreaPadding(.all, 20)
}
```
#### Nested Padding (Combined Approach)
```swift
// Outer: Safe area padding for device insets
VStack(spacing: 0) {
content
}
.safeAreaPadding(.horizontal, 16) // Beyond safe areas
// Inner: Regular padding for internal spacing
VStack {
Text("Title")
.padding(.bottom, 8) // Internal spacing
Text("Subtitle")
}
```
### Decision Tree
```
Does your content extend to screen edges?
├─ YES → Use .safeAreaPadding()
│ ├─ Is it scrollable? → .safeAreaPadding(.horizontal/.vertical)
│ └─ Is it full-screen? → .safeAreaPadding(.all)
│
└─ NO (contained within a safe container like List/Form)
└─ Use .padding() for internal spacing
```
### Visual Debugging
```swift
// Visualize safe area padding (iOS 17+)
content
.safeAreaPadding(.horizontal, 20)
.background(.red.opacity(0.2)) // Shows padding area
.border(.blue) // Shows content bounds
```
### Migration from Manual Safe Area Handling
```swift
// ❌ OLD: Manual calculation (iOS 13-16)
GeometryReader { geo in
content
.padding(.top, geo.safeAreaInsets.top + 16)
.padding(.bottom, geo.safeAreaInsets.bottom + 16)
.padding(.horizontal, 20)
}
// ✅ NEW: .safeAreaPadding() (iOS 17+)
content
.safeAreaPadding(.vertical, 16)
.safeAreaPadding(.horizontal, 20)
```
### Related APIs
**`.safeAreaInset(edge:)`** - Adds persistent content that shrinks the safe area:
```swift
ScrollView {
content
}
.safeAreaInset(edge: .bottom) {
// This REDUCES the safe area, content scrolls under it
toolbarButtons
.padding()
.background(.ultraThinMaterial)
}
```
**`.ignoresSafeArea()`** - Opts out of safe area completely:
```swift
Color.blue
.ignoresSafeArea() // Extends to absolute screen edges
```
### Why It Matters
**Before iOS 17**: Developers had to manually calculate safe area insets with GeometryReader, leading to:
- Verbose code
- Performance overhead (GeometryReader forces extra layout pass)
- Easy mistakes (forgetting to check all edges)
**iOS 17+**: `.safeAreaPadding()` provides:
- Declarative API (matches SwiftUI philosophy)
- Automatic safe area awareness
- Better performance (no extra layout passes)
- Type-safe edge specification
**Real-world impact**: Using `.padding()` instead of `.safeAreaPadding()` on iPhone 15 Pro causes content to:
- Hit the Dynamic Island (top)
- Overlap the home indicator (bottom)
- Get cut off by screen corners (rounded edges)
---
## Size Classes
Environment values indicating horizontal and vertical size characteristics.
### Reading Size Classes
```swift
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
compactLayout
} else {
regularLayout
}
}
}
```
### Size Class Values
```swift
enum UserInterfaceSizeClass {
case compact // Constrained space
case regular // Ample space
}
```
### Platform Behavior
**iPhone:**
| Orientation | Horizontal | Vertical |
|-------------|------------|----------|
| Portrait | `.compact` | `.regular` |
| Landscape (small) | `.compact` | `.compact` |
| Landscape (Plus/Max) | `.regular` | `.compact` |
**iPad:**
| Configuration | Horizontal | Vertical |
|--------------|------------|----------|
| Any full screen | `.regular` | `.regular` |
| 70% Split View | `.regular` | `.regular` |
| 50% Split View | `.regular` | `.regular` |
| 33% Split View | `.compact` | `.regular` |
| Slide Over | `.compact` | `.regular` |
### Overriding Size Classes
```swift
content
.environment(\.horizontalSizeClass, .compact)
```
---
## Dynamic Type Size
Environment value for user's preferred text size.
### Reading Dynamic Type
```swift
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
accessibleLayout
} else {
standardLayout
}
}
```
### Size Categories
```swift
enum DynamicTypeSize: Comparable {
case xSmall
case small
case medium
case large // Default
case xLarge
case xxLarge
case xxxLarge
case accessibility1 // isAccessibilitySize = true
case accessibility2
case accessibility3
case accessibility4
case accessibility5
}
```
### Scaled Metric
```swift
@ScaledMetric var iconSize: CGFloat = 24
@ScaledMetric(relativeTo: .largeTitle) var headerSize: CGFloat = 44
Image(systemName: "star")
.frame(width: iconSize, height: iconSize)
```
---
## iOS 26 Window APIs
### Window Resize Anchor
```swift
WindowGroup {
ContentView()
}
.windowResizeAnchor(.topLeading) // Resize originates from top-left
.windowResizeAnchor(.center) // Resize from center
```
### Menu Bar Commands (iPad)
```swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("View") {
Button("Show Sidebar") {
showSidebar.toggle()
}
.keyboardShortcut("s", modifiers: [.command, .option])
Divider()
Button("Zoom In") { zoom += 0.1 }
.keyboardShortcut("+")
Button("Zoom Out") { zoom -= 0.1 }
.keyboardShortcut("-")
}
}
}
}
```
### NavigationSplitView Column Control
```swift
// iOS 26: Automatic column visibility
NavigationSplitView {
Sidebar()
} content: {
ContentList()
} detail: {
DetailView()
}
// Columns auto-hide/show based on available width
// Manual control (when needed)
@State private var columnVisibility: NavigationSplitViewVisibility = .all
NavigationSplitView(columnVisibility: $columnVisibility) {
Sidebar()
} detail: {
DetailView()
}
```
### Scene Phase
```swift
@Environment(\.scenePhase) var scenePhase
var body: some View {
content
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
// Window is visible and interactive
case .inactive:
// Window is visible but not interactive
case .background:
// Window is not visible
}
}
}
```
---
## Coordinate Spaces
### Built-in Coordinate Spaces
```swift
// Global (screen coordinates)
proxy.frame(in: .global)
// Local (view's own bounds)
proxy.frame(in: .local)
// Named (custom)
proxy.frame(in: .named("mySpace"))
```
### Creating Named Spaces
```swift
ScrollView {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .named("scroll")).minY
} action: { offset in
scrollOffset = offset
}
}
.coordinateSpace(name: "scroll")
// iOS 17+ typed coordinate space
extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace {
static var scroll: Self { .named("scroll") }
}
```
---
## ScrollView Geometry (iOS 18+)
### onScrollGeometryChange
```swift
ScrollView {
content
}
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y
} action: { offset in
scrollOffset = offset
}
```
### ScrollGeometry Properties
```swift
.onScrollGeometryChange(for: ScrollGeometry.self) { $0 } action: { geo in
let offset = geo.contentOffset // Current scroll position
let size = geo.contentSize // Total content size
let visible = geo.visibleRect // Currently visible rect
let insets = geo.contentInsets // Content insets
}
```
---
## Lazy Container Gotchas
### Recycling Behavior
`LazyVStack` and `LazyHStack` create views **on demand** and recycle them when off-screen. This means:
- **View identity matters**: If cells flash/disappear during fast scrolling, the view identity is unstable. Use explicit `.id()` on items.
- **onAppear/onDisappear fire repeatedly**: Views are created and destroyed as you scroll. Don't use these for one-time setup.
- **State resets on recycle**: `@State` in lazy items resets when recycled. Lift state to the model layer.
```swift
// ❌ Items flash during fast scroll — unstable identity
LazyVStack {
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
ItemRow(item: item) // Identity changes when array mutates
}
}
// ✅ Stable identity prevents flash/disappear
LazyVStack {
ForEach(items) { item in // Uses item.id (Identifiable)
ItemRow(item: item)
}
}
```
### When NOT to Use Lazy Containers
| Scenario | Use Instead | Why |
|----------|-------------|-----|
| < 50 items | `VStack` / `HStack` | No recycling overhead, simpler |
| Nested in another lazy container | `VStack` (inner) | Nested lazy causes layout issues |
| Need all items measured upfront | `VStack` | Lazy containers don't know total size |
---
## Resources
**WWDC**: 2025-208, 2024-10074, 2022-10056
**Docs**: /swiftui/layout, /swiftui/viewthatfits
**Skills**: axiom-swiftui-layout, axiom-swiftui-debuggingRelated Skills
dashboard-layout-planner
Dashboard Layout Planner - Auto-activating skill for Data Analytics. Triggers on: dashboard layout planner, dashboard layout planner Part of the Data Analytics skill category.
Expo UI SwiftUI
`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.
hig-components-layout
Apple Human Interface Guidelines for layout and navigation components. Use this skill when the user asks about sidebar, split view, tab bar, tab view, scroll view, window design, panel, list view, table view, column view, outline view, navigation structure, app layout, boxes, ornaments, or organizing content hierarchically in Apple apps. Also use when the user says how should I organize my app, what navigation pattern should I use, my layout breaks on iPad, how do I build a sidebar, should I use tabs or a sidebar, or my app doesn't adapt to different screen sizes. Cross-references: hig-foundations for layout/spacing principles, hig-platforms for platform-specific navigation, hig-patterns for multitasking and full-screen, hig-components-content for content display.
avalonia-layout-zafiro
Guidelines for modern Avalonia UI layout using Zafiro.Avalonia, emphasizing shared styles, generic components, and avoiding XAML redundancy.
writing-page-layout
Use this skill when you need to write code for a page layout in the Next.js
swiftui-view-refactor
Refactor and review SwiftUI view files for consistent structure, dependency injection, and Observation usage. Use when asked to clean up a SwiftUI view’s layout/ordering, handle view models safely (non-optional when possible), or standardize how dependencies and @Observable state are initialized and passed.
swiftui-ui-patterns
Best practices and example-driven guidance for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture with TabView, composing screens, or needing component-specific patterns and examples.
swiftui-performance-audit
Audit and improve SwiftUI runtime performance from code review and architecture. Use for requests to diagnose slow rendering, janky scrolling, high CPU/memory usage, excessive view updates, or layout thrash in SwiftUI apps, and to provide guidance for user-run Instruments profiling when code review alone is insufficient.
swiftui-liquid-glass
Implement, review, or improve SwiftUI features using the iOS 26+ Liquid Glass API. Use when asked to adopt Liquid Glass in new SwiftUI UI, refactor an existing feature to Liquid Glass, or review Liquid Glass usage for correctness, performance, and design alignment.
axiom-audit
Audit Axiom logs to identify and prioritize errors and warnings, research probable causes, and flag log smells. Use when user asks to check Axiom logs, analyze production errors, investigate log issues, or audit logging patterns.
swiftui-patterns
SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.
Axiom — Serverless Log Analytics
## Overview