/layout
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
$ npx -y skills add rshankras/claude-code-apple-skills --skill layout --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/layout
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
layout.SKILL.mdname: layout
description: 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.
allowed-tools: [Read, Write, Edit, Glob, Grep]
last_verified: 2026-07-16
review_by: 2027-06-22
SwiftUI Layout & Containers
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`.
When This Skill Activates
- "Make these buttons equal width" / measurement-dependent layout
- Building a reusable container (custom List/board/carousel) that should accept ForEach + sections
- Lazy stack jank, memory growth, scroll-position bugs, broken scroll targeting
- Programmatic scrolling, paging/snapping, scroll-linked effects
- GeometryReader causing layout loops or mangled sizing
Custom Layout protocol (not GeometryReader)
Reach for a custom `Layout` whenever you must **measure subviews and feed the measurement back into layout** — GeometryReader only measures its container and can't influence the engine. 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).
- **Respect spacing preferences**: `subviews[i].spacing.distance(to:along:)`, taking the larger
of conflicting preferences — matching built-in containers. No hardcoded 8s.
- Per-subview data via `LayoutValueKey` (+ a `layoutValue` convenience modifier), read as
`subview[Key.self]`.
- Cache only after Instruments shows layout cost — it's an optimization, not a requirement.
- **Switch layouts without killing identity**: `AnyLayout(HStackLayout())` ↔ custom layout with
`.animation(_:value:)` — SwiftUI sees one changing view, so state survives and it animates.
- Don't build fallbacks into the layout — wrap alternatives in `ViewThatFits`.
Grid decisions
| 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` |
Custom containers (Demystify Containers)
Make containers that compose like `List` does:
- API shape: a trailing `@ViewBuilder var content: Content` — callers can then mix static
views, `ForEach`, and conditionals.
- Iterate **resolved** children with `ForEach(subviews: content)`; need the whole collection
(count/chunking)? `Group(subviews: content) { subviews in … }`.
- Internalize **declared vs resolved**: one declared ForEach resolves to N subviews; Group to
its children; EmptyView to zero; `if` conditionally. Counting declared views is a bug.
- Sections are opt-in: `ForEach(sections: content)`, reading `section.header` /
`section.content`; check `header.isEmpty` before rendering the slot.
- Per-child customization via container values: `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.
Lazy stacks & scrolling performance (WWDC26 rules)
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:
- **One subview per ForEach element, always.** An `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.
- **Never key logic off absolute scroll offset** in a lazy stack (`onScrollGeometryChange` sees
estimates) — use `onScrollTargetVisibilityChange(threshold: 0.8)` for visibility triggers.
- **Set up in `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`.
- **Don't persist meaningful state in row `@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).
- Don't drive layout from `onGeometryChange` height feedback (content shoves, targeting
breaks) — that's the custom `Layout` case above.
- Nest `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.
The scroll API map
- Snapping/paging: `scrollTargetLayout()` + `scrollTargetBehavior(.viewAligned/.paging)`.
- Track/control position: `scrollPosition` binding; programmatic `ScrollPosition` +
`scrollTo(id:)` — works for unloaded targets *if* IDs map to stable one-subview el
Read more
name: layout description: 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. allowed-tools: [Read, Write, Edit, Glob, Grep] last_verified: 2026-07-16 review_by: 2027-06-22
SwiftUI Layout & Containers
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`.
When This Skill Activates
- "Make these buttons equal width" / measurement-dependent layout
- Building a reusable container (custom List/board/carousel) that should accept ForEach + sections
- Lazy stack jank, memory growth, scroll-position bugs, broken scroll targeting
- Programmatic scrolling, paging/snapping, scroll-linked effects
- GeometryReader causing layout loops or mangled sizing
Custom Layout protocol (not GeometryReader)
Reach for a custom `Layout` whenever you must **measure subviews and feed the measurement back into layout** — GeometryReader only measures its container and can't influence the engine. 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).
- **Respect spacing preferences**: `subviews[i].spacing.distance(to:along:)`, taking the larger
of conflicting preferences — matching built-in containers. No hardcoded 8s.
- Per-subview data via `LayoutValueKey` (+ a `layoutValue` convenience modifier), read as
`subview[Key.self]`.
- Cache only after Instruments shows layout cost — it's an optimization, not a requirement.
- **Switch layouts without killing identity**: `AnyLayout(HStackLayout())` ↔ custom layout with
`.animation(_:value:)` — SwiftUI sees one changing view, so state survives and it animates.
- Don't build fallbacks into the layout — wrap alternatives in `ViewThatFits`.
Grid decisions
| 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` |
Custom containers (Demystify Containers)
Make containers that compose like `List` does:
- API shape: a trailing `@ViewBuilder var content: Content` — callers can then mix static
views, `ForEach`, and conditionals.
- Iterate **resolved** children with `ForEach(subviews: content)`; need the whole collection
(count/chunking)? `Group(subviews: content) { subviews in … }`.
- Internalize **declared vs resolved**: one declared ForEach resolves to N subviews; Group to
its children; EmptyView to zero; `if` conditionally. Counting declared views is a bug.
- Sections are opt-in: `ForEach(sections: content)`, reading `section.header` /
`section.content`; check `header.isEmpty` before rendering the slot.
- Per-child customization via container values: `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.
Lazy stacks & scrolling performance (WWDC26 rules)
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:
- **One subview per ForEach element, always.** An `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.
- **Never key logic off absolute scroll offset** in a lazy stack (`onScrollGeometryChange` sees
estimates) — use `onScrollTargetVisibilityChange(threshold: 0.8)` for visibility triggers.
- **Set up in `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`.
- **Don't persist meaningful state in row `@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).
- Don't drive layout from `onGeometryChange` height feedback (content shoves, targeting
breaks) — that's the custom `Layout` case above.
- Nest `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.
The scroll API map
- Snapping/paging: `scrollTargetLayout()` + `scrollTargetBehavior(.viewAligned/.paging)`.
- Track/control position: `scrollPosition` binding; programmatic `ScrollPosition` +
`scrollTo(id:)` — works for unloaded targets *if* IDs map to stable one-subview el
A collection of Claude Code skills for iOS, macOS, watchOS, visionOS, and Apple platform development. These skills help you plan and build apps, maintain code quality, ensure HIG compliance, and guide you from idea to App Store.
Repo: rshankras/claude-code-apple-skills
Other skills on rshankras-apple-skills.
- /app-store
App Store optimization and marketing skills for descriptions, screenshots, keywords, review responses, and comprehensive promotional strategy. Use when user needs help with App Store presence, ASO, marketing, or customer communication.
Open skill - /ad-attribution
Privacy-preserving ad measurement with AdAttributionKit (SKAdNetwork's successor) — install and re-engagement attribution, conversion-value strategy under crowd anonymity, and end-to-end postback testing. Use when running paid acquisition beyond Apple Ads, measuring
Open skill - /app-description-writer
Generate compelling App Store descriptions that convert browsers into users. Use when writing initial descriptions, improving existing copy, or drafting promotional text and What's New for a major update.
Open skill - /apple-search-ads
Apple Search Ads campaign strategy for indie developers — paid acquisition, keyword bidding, budget planning, and ROAS optimization. Use when user asks about running ads, paid user acquisition, or Apple Search Ads campaigns.
Open skill - /iap-finalizer
Take a one-time in-app purchase from MISSING_METADATA to READY_TO_SUBMIT in App Store Connect — set its price schedule and localized display name/description (and optional review screenshot) via the ASC REST API. Use at Phase 6 (Pre-Release), after the IAP is built in-app (Phase
Open skill - /keyword-optimizer
Optimize app title, subtitle, and keywords for maximum App Store discoverability. Use when launching a new app, improving search rankings, entering new markets/languages, or safely optimizing ASO for an app with existing traffic.
Open skill

