swiftship/internal/skills/data/ui/adaptive-layout/SKILL.md
Adaptive layout for iPad and universal apps: NavigationSplitView, size classes, horizontalSizeClass, presentations, HIG compliance. Use when building iPad-optimized layouts, supporting multiple screen sizes, or adapting UI for iPhone+iPad. Triggers: iPad, universal, NavigationSplitView, horizontalSizeClass, size class, adaptive.
npx skillsauth add abdullah4ai/apple-dev-docs adaptive-layoutInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Let SwiftUI adapt automatically. Never check UIDevice.current.userInterfaceIdiom or UIScreen.main.bounds for layout decisions. Use size classes and adaptive containers instead.
Use NavigationSplitView for ANY list-detail flow. It collapses to NavigationStack on iPhone and shows sidebar+detail on iPad — zero conditional code.
@State private var selectedItem: Item?
NavigationSplitView {
List(items, selection: $selectedItem) { item in
NavigationLink(value: item) { ItemRow(item: item) }
}
.navigationTitle("Items")
} detail: {
if let selectedItem {
ItemDetailView(item: selectedItem)
} else {
ContentUnavailableView("Select an Item", systemImage: "doc")
}
}
Three-column pattern (sidebar categories → list → detail):
NavigationSplitView {
List(categories, selection: $selectedCategory) { ... }
} content: {
List(filteredItems, selection: $selectedItem) { ... }
} detail: {
if let selectedItem { DetailView(item: selectedItem) }
else { ContentUnavailableView(...) }
}
Use NavigationStack ONLY for purely linear flows (onboarding, checkout).
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
| Context | horizontalSizeClass |
|---|---|
| iPhone portrait | .compact |
| iPhone landscape | .compact |
| iPad full-screen | .regular |
| iPad Split View (narrow) | .compact |
| iPad Split View (wide) | .regular |
Critical: iPad in Split View multitasking can report .compact. This is why UIDevice.current is wrong — always use size classes.
@Environment(\.horizontalSizeClass) private var sizeClass
var body: some View {
let layout = sizeClass == .compact
? AnyLayout(VStackLayout(spacing: 16))
: AnyLayout(HStackLayout(spacing: 24))
layout {
ContentBlockA()
ContentBlockB()
}
}
Use when the decision is purely about available space — no environment reading needed:
ViewThatFits {
HStack(spacing: 16) { icon; title; subtitle; actionButton }
VStack(alignment: .leading, spacing: 8) {
HStack { icon; title }
subtitle
actionButton
}
}
When to use which:
ViewThatFits → component-level (a card, a header, a toolbar item)horizontalSizeClass → screen-level (different page structures)Always use GridItem(.adaptive(minimum:maximum:)) — never hardcode column counts:
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 160, maximum: 320))],
spacing: 16
) {
ForEach(items) { item in CardView(item: item) }
}
.padding()
Automatically: 2 columns on iPhone, 3-4 on iPad portrait, 4-6 on iPad landscape.
.popover(isPresented: $showOptions) {
OptionsView()
}
.sheet(isPresented: $showSheet) {
SheetContent()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
Always use .confirmationDialog — it becomes an action sheet on iPhone, popover on iPad:
.confirmationDialog("Options", isPresented: $showDialog) {
Button("Option A") { }
Button("Cancel", role: .cancel) { }
}
.padding() with no arguments — SwiftUI applies 16pt on iPhone, 20pt on iPad automatically.ScrollView {
content
.frame(maxWidth: 700)
.frame(maxWidth: .infinity)
}
@ScaledMetric for custom dimensions that respect Dynamic Type:@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = 24
Prefer containerRelativeFrame over GeometryReader:
Image("photo")
.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 16)
Reserve GeometryReader only for complex calculations (parallax, custom alignment).
UIDevice.current.userInterfaceIdiom for layout — breaks iPad multitaskingUIScreen.main.bounds for sizing — doesn't respond to multitasking#if targetEnvironment for layout decisions.frame(width: 375)).adaptive(minimum:).font(.body), .font(.title))NavigationSplitView for iPad empty state.popover for contextual actions — SwiftUI auto-adapts.leading/.trailing — never .left/.righttesting
Use for 3D games: racing, 3D sports, board games, marble maze, tower defense, bowling. SceneKit + SceneView architecture, 3D scene hierarchy, physics, game loop, primitives, materials, cameras, particles, audio.
documentation
Game UI patterns: SwiftUI HUD overlays on SpriteKit, menus (main/pause/game-over), virtual joystick/d-pad, score displays, health bars, tutorial onboarding.
tools
Download free game sprites/textures/3D models and generate procedural assets. Covers nw_download_asset tool, texture factories, sprite atlas organization, 3D model loading, and programmatic asset creation.
testing
Use for 2D games: arcade, puzzle, sports, ping pong, platformer, shooter, 2D racing. SpriteKit + SpriteView architecture, scene hierarchy, physics, game loop, audio, particles, game feel.