rshankras/claude-code-apple-skills/layout
Claude Code and agent skill by rshankras/claude-code-apple-skills
- community
Checking that the SKILL.md file is still in the repository.
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.
Install
npx vskill@latest install rshankras/claude-code-apple-skills/layoutSKILL.md
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.unspecifiedto 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) — usebounds.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(+ alayoutValueconvenience 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), readingsection.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
ifinside 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 (
onScrollGeometryChangesees
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).
scrollTransitiontransforms must stay inside the original frame (scale ✅; rotations
escaping the frame make views vanish early).
- Don't drive layout from
onGeometryChangeheight feedback (content shoves, targeting
breaks) — that's the custom Layout case above.
- Nest
LazyHStackinsideLazyVStackfreely (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:
scrollPositionbinding; programmaticScrollPosition+
scrollTo(id:) — works for unloaded targets if IDs map to stable one-subview elements.
- Scroll-linked effects:
scrollTransition(enter/leave viewport) andvisualEffect
(geometry without GeometryReader) — details in design/animation-patterns.
- Reactions:
onScrollGeometryChange(fine outside lazy estimation),
onScrollVisibilityChange (autoplay/analytics).
- Performance floor: list/scroll internals were rewritten (WWDC25) — macOS lists ~6× faster at
100k+ rows, and lazy loading works in nested ScrollView+LazyVStack; profile with the SwiftUI instrument (performance/swiftui-debugging).
Output Format
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.
References
- https://developer.apple.com/videos/play/wwdc2022/10056/ (Compose custom layouts)
- https://developer.apple.com/videos/play/wwdc2024/10146/ (Demystify SwiftUI containers)
- https://developer.apple.com/videos/play/wwdc2026/321/ (Dive into lazy stacks and scrolling)
- Related skills:
swiftui/data-flow(identity/ForEach IDs),performance/swiftui-debugging,design/animation-patterns(scroll-linked effects)
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/swiftui/layout/SKILL.md
Popularity
- Repository stars
- 770
- Repository forks
- 74
- 7-day trend score
- 33
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/rshankras/claude-code-apple-skills/layout)