skills/swiftui/layout/SKILL.md
SwiftUI layout beyond stacks — the Layout protocol (when custom layout beats GeometryReader), Grid vs lazy grids, custom containers with sections and container values, and lazy-stack/ScrollView performance rules (what breaks laziness, prefetch discipline, scroll APIs). Use when building custom layouts or containers, fixing lazy-stack jank or memory growth, or wiring programmatic/snapping scrolling.
npx skillsauth add rshankras/claude-code-apple-skills 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.
The layer between "stacks and spacers" and "it scrolls like butter with 100k rows" — Apple's
Layout protocol, container composition, and the lazy-stack rules from the WWDC26 deep dive.
View identity/data-flow questions route to swiftui/data-flow.
Reach for a custom Layout whenever you must measure subviews and feed the measurement back
into layout — GeometryReader measures its container and can't influence the engine
(feedback through state risks layout loops). Canonical case: equal-width buttons.
sizeThatFits: propose .unspecified to read each subview's ideal size
(subviews.map { $0.sizeThatFits(.unspecified) }); guard empty subviews;
replacingUnspecifiedDimensions() for nil proposal dimensions.placeSubviews: never assume origin (0,0) — use bounds.minX/midX (non-zero origins are
what make layouts composable); place(at:anchor:proposal:) with a proposal that may differ
from the ideal size (that's how equal widths happen).subviews[i].spacing.distance(to:along:), taking the larger
of conflicting preferences — matching built-in containers. No hardcoded 8s.LayoutValueKey (+ a layoutValue convenience modifier), read as
subview[Key.self].AnyLayout(HStackLayout()) ↔ custom layout with
.animation(_:value:) — SwiftUI sees one changing view, so state survives and it animates.ViewThatFits.| Need | Use |
|---|---|
| Static 2D with cross-row alignment | Grid/GridRow (+ gridCellColumns to span, gridColumnAlignment per column) |
| Scrollable, large content | LazyVGrid/LazyHGrid (only visible views load; one axis fixed up front) |
| "First arrangement that fits" | ViewThatFits |
Make containers that compose like List does:
@ViewBuilder var content: Content — callers can then mix static
views, ForEach, and conditionals.ForEach(subviews: content); need the whole collection
(count/chunking)? Group(subviews: content) { subviews in … }.if conditionally. Counting declared views is a bug.ForEach(sections: content), reading section.header /
section.content; check header.isEmpty before rendering the slot.extension ContainerValues { @Entry var … },
set with a convenience modifier, read via subview.containerValues. Scoping model:
Environment flows down · Preferences flow up · container values reach only the direct
container. Setting one on a Section styles the whole section.LazyVStack builds views only until the viewport fills; totals and offsets are estimated from average placed-view size and corrected as you scroll. Everything below follows from that:
if inside a row (0-or-1 views) forces the
stack to keep off-screen views + their @State alive to preserve indices — and environment
changes then re-evaluate off-screen bodies. Filter at the data layer (@Query predicate);
gate auth-type conditions outside the stack.onScrollGeometryChange sees
estimates) — use onScrollTargetVisibilityChange(threshold: 0.8) for visibility triggers.init, not onAppear (_model = State(initialValue:)): body runs during
prefetch; onAppear fires only on-screen, throwing prefetch work away and causing
post-appearance size jumps. Start async loads in init/task.@State — off-screen views are eventually
released. Hoist (@State var highlighted: Set<ID> outside, @Binding down).scrollTransition transforms must stay inside the original frame (scale ✅; rotations
escaping the frame make views vanish early).onGeometryChange height feedback (content shoves, targeting
breaks) — that's the custom Layout case above.LazyHStack inside LazyVStack freely (unscrolled rows stay unloaded) — but fix child
heights (lineLimit, explicit frames) in the horizontal stacks.pinnedViews: [.sectionHeaders] pins headers; infinite scroll = trailing
ProgressView().onAppear { fetchNextPage() } after the ForEach.scrollTargetLayout() + scrollTargetBehavior(.viewAligned/.paging).scrollPosition binding; programmatic ScrollPosition +
scrollTo(id:) — works for unloaded targets if IDs map to stable one-subview elements.scrollTransition (enter/leave viewport) and visualEffect
(geometry without GeometryReader) — details in design/animation-patterns.onScrollGeometryChange (fine outside lazy estimation),
onScrollVisibilityChange (autoplay/analytics).ScrollView+LazyVStack; profile with the
SwiftUI instrument (performance/swiftui-debugging).Layout review: Symptom | Rule violated | Fix — check the one-subview-per-element rule first
in any lazy-stack complaint; it explains most jank, memory growth, and targeting bugs.
swiftui/data-flow (identity/ForEach IDs), performance/swiftui-debugging, design/animation-patterns (scroll-linked effects)development
US web checkout via the StoreKit External Purchase Link entitlement — currently 0% Apple commission (litigation ongoing), how to ship it safely, and how to architect for a commission flip so a future ruling is a config change, not a rewrite. Use when adding external purchase links, weighing web checkout vs IAP, or planning US-storefront pricing strategy.
tools
Revenue beyond the single-app price tag — own-app bundles, Family Sharing as a conversion lever, cross-developer bundles & suites, and institutional licensing via Group Purchases / Apple School & Business Manager. Use when a developer has multiple apps, a subscription worth sharing, complementary indie partners, or school/clinic/business buyers.
testing
Run a structured accessibility audit on an iOS/macOS app — automated XCUITest audits, Accessibility Inspector, manual VoiceOver/Dynamic Type passes, and App Store Accessibility Nutrition Label evaluation. Use before release, when preparing Nutrition Label declarations, or for EU Accessibility Act compliance.
tools
Stage-by-stage audit of an app's App Store growth machinery against a 54-item P0–P9 playbook — every item scored from an App Store Connect MCP call, a codebase check, or an explicit question to the user, then routed to the skill or command that fixes it. Read-only on App Store Connect. Use for a growth audit or scorecard, a pre-launch growth plan, a quarterly re-audit, or "which growth levers am I missing."