agentsclimarketplace

Swiftui layout

Skill Tyr0/agent-skills/plugins/swiftui-expert/skills/swiftui-layout

A collection of skills, plugins, and agents for AI workflows.

Install
npx -y skills add Tyr0/agent-skills --skill swiftui-layout

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use this skill whenever the user asks about SwiftUI layout — how views are sized and positioned, the proposed-size / required-size negotiation, `frame` vs `fixedSize`, `Spacer`, `padding`, alignment guides, safe areas, `GeometryReader`, the custom `Layout` protocol, `containerRelativeFrame`, `onGeometryChange`, scroll-driven layout, lazy grids, or any "why is my view the wrong size" question. Targets iOS 17+. Triggers on "how does SwiftUI layout work", "why is my view full-width", "what does fixedSize do", "how do I align two views", "should I use GeometryReader", or "how do I write a custom Layout".

SKILL.md

10.8 KB, as published. Nobody here has run it

SwiftUI Layout

The SwiftUI layout system is a two-pass negotiation. Once the model clicks, the rest is just modifiers applied in the right order.


The Layout Algorithm

Every layout is a conversation between a parent and a child:

  1. Parent proposes a size — "you may use up to W×H" (either dimension can be nil = "as much as you want", or a concrete value).
  2. Child returns its required size — it picks. The parent does not get to override.
  3. Parent positions the child within its own bounds.

The child is sovereign about its size. The parent is sovereign about position. There is no "force this child to be 100 points wide" — there's only "I propose 100, you decide."

This is the mental model. Memorize it.

Examples

  • Text("Hi") — proposed full available space, returns just enough for the text.
  • Color.blue — proposed N, returns N (greedy; takes whatever's offered).
  • Image("foo") — returns the image's intrinsic size unless .resizable().
  • Image("foo").resizable() — returns the proposed size.
  • Spacer() — returns the proposed size in its stack's axis, 0 perpendicular.

Why your view is full-width

Color, Rectangle, Spacer, and .background(Color) are greedy. Stack them with a Text and the stack proposes the full width to the greedy view. The fix is .frame(maxWidth: ...) or .fixedSize() on the appropriate child.


frame Modifiers

.frame doesn't force a size on its child. It inserts a wrapper view between parent and child, and the wrapper does its own proposal.

Text("Hi").frame(width: 100, height: 50)

Wrapper view: proposes (100, 50) to Text. Text returns its required (small) size. Wrapper reports (100, 50) to its parent and centers the Text inside.

Forms

FormBehavior
.frame(width:height:)Proposes exact size; reports exact size
.frame(maxWidth: .infinity)Proposes whatever the outer parent gave; reports same
.frame(minWidth:, maxWidth:)Clamps
.frame(maxWidth: .infinity, alignment: .leading)Greedy + child aligned within

fixedSize

Tells the view to ignore its proposal and use its intrinsic (ideal) size.

Text(longString)
    .fixedSize(horizontal: false, vertical: true)  // wrap is fine, but never truncate vertically

Use case: text that should wrap to its full content rather than being truncated by a constraining parent.


Stacks

HStack / VStack / ZStack divide proposed space among children using a "least-flexible-first" allocation:

  1. Sort children by flexibility (smallest range first).
  2. Propose total / remainingChildren to each in order.
  3. Remaining children share leftover space.

This is why a Text in an HStack with a Spacer lays out predictably: Text is rigid, Spacer is flexible.

spacing: controls gaps. .layoutPriority(1) raises a child's priority so it gets space first.

ZStack overlays children with alignment; the ZStack's size is the union of its children's sizes.

LazyVStack / LazyHStack / LazyVGrid / LazyHGrid

Same model, but children are realized only when scrolled near. Use inside a ScrollView. Required for tens of thousands of rows.


Alignment

Alignment is per-axis and uses alignment guides:

HStack(alignment: .firstTextBaseline) { ... }
VStack(alignment: .leading) { ... }
ZStack(alignment: .topTrailing) { ... }

Custom alignment guides let you align non-adjacent views or align across nested stacks:

extension HorizontalAlignment {
    enum FormLabel: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat { d[.leading] }
    }
    static let formLabel = HorizontalAlignment(FormLabel.self)
}

VStack(alignment: .formLabel) {
    HStack { Text("Name").alignmentGuide(.formLabel) { $0[.trailing] }
             TextField("", text: $name) }
    HStack { Text("Email").alignmentGuide(.formLabel) { $0[.trailing] }
             TextField("", text: $email) }
}

Result: labels right-aligned to each other, fields aligned to a shared column.


Safe Area & Insets

By default, content respects the device's safe area (notches, home indicator, keyboard). To extend visuals into the safe area:

SomeBackground().ignoresSafeArea()
SomeBackground().ignoresSafeArea(.keyboard, edges: .bottom)

Use .safeAreaInset(edge:) to stick a view to a safe area edge while pushing content out of its way:

ScrollView { Content() }
    .safeAreaInset(edge: .bottom) { BottomBar() }

.safeAreaPadding (iOS 17+) adds padding inside the safe area without altering geometry.


padding

padding adds insets outside the modified view (from the wrapper's perspective: wrapper is bigger by N, child gets the original proposal minus 2N).

Text("Hi").padding()                   // system-default all sides
Text("Hi").padding(.horizontal, 16)
Text("Hi").padding(.bottom, 8)

Order matters: .padding().background(.red) paints around the padding; .background(.red).padding() paints only behind the text and adds outer padding.


GeometryReader (and What to Use Instead)

GeometryReader is greedy in both axes and gives you the proposed size + safe-area insets. It distorts layout because it always takes whatever the parent offers.

GeometryReader { proxy in
    Text("\(proxy.size.width)")
}

Problems:

  • Inside a VStack, fills the whole stack vertically.
  • Reading geometry to write @State fires update warnings.
  • Re-runs on every layout pass; expensive in lists.

Modern alternatives (iOS 17+):

NeedUse
Size relative to scroll/window container.containerRelativeFrame(.horizontal) { length, _ in length * 0.8 }
Read geometry without distorting layout (iOS 18).onGeometryChange(for: CGSize.self) { $0.size } action: { newSize in ... }
Coordinate space transformsGeometryReader is still fine, but isolate it
Build a measurement-driven layoutImplement Layout protocol

Custom Layout Protocol (iOS 16+)

When you need fully custom positioning (a flow layout, radial layout, custom split):

struct FlowLayout: Layout {
    var spacing: CGFloat = 8

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        // Return required size for the subviews given the proposal.
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        for s in subviews { s.place(at: ..., proposal: ...) }
    }
}

FlowLayout(spacing: 8) { ForEach(tags) { TagView($0) } }

Layout is faster than GeometryReader-based emulations because it participates in the real layout pass and doesn't force re-renders.


Grids

Two grid systems:

  • Grid (iOS 16+) — eager grid with row/column alignment guarantees. Like CSS grid.
    Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 8) {
        GridRow { Text("Name");  TextField("", text: $name) }
        GridRow { Text("Email"); TextField("", text: $email) }
    }
    
  • LazyVGrid / LazyHGrid — lazy, configured with [GridItem].
    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { ForEach(items) { ... } }
    

GridItem.Size cases: .fixed, .flexible(minimum:maximum:), .adaptive(minimum:maximum:).


ScrollView

ScrollView proposes its content axis as nil (unbounded) and the cross axis as the proposed value.

iOS 17+ scroll APIs:

  • .scrollPosition(id:) — bind a leading-cell ID.
  • .scrollTargetBehavior(.viewAligned) — snap-to-page-like behavior.
  • .scrollTargetLayout() — mark the layout that contains scroll targets.
  • .scrollTransition { content, phase in ... } — phase-driven transitions per cell.
  • .containerRelativeFrame — size cells by the visible container.
  • .scrollIndicators(.hidden) — hide indicators.
  • .scrollBounceBehavior(.basedOnSize) — only bounce when content overflows.

Common Layout Recipes

GoalApproach
Center a viewColor.clear.overlay(view) or stack with Spacer()s
Fill available width.frame(maxWidth: .infinity)
Fill width but cap.frame(maxWidth: 600)
Two columns 50/50HStack { A; B } with each .frame(maxWidth: .infinity)
Pin to bottom.safeAreaInset(edge: .bottom) { BottomBar() }
Sticky header in ScrollViewLazyVStack(pinnedViews: [.sectionHeaders]) + Section
Aspect-fit imageImage.resizable().aspectRatio(contentMode: .fit)
Square cell.aspectRatio(1, contentMode: .fit)
Wrap text and stop truncation.fixedSize(horizontal: false, vertical: true)
Match width of siblingCustom PreferenceKey or Grid

Anti-Patterns

Anti-patternProblemFix
GeometryReader wrapping a whole screenGreedy in both axes; distorts; reads on every framecontainerRelativeFrame, onGeometryChange, or Layout
.frame(width:) to force child sizeThe child still chooses; you only constrain the proposalUse fixedSize for intrinsic, or accept child sovereignty
Spacer() to pad inside an HStack instead of .paddingSpacers grow unpredictably with siblings.padding(.horizontal, ...)
.padding then .background (or vice-versa, mixed up)Wrong color extentOrder modifiers deliberately and remember they are nested wrappers
Animating layout that depends on GeometryReader sizeCauses layout-during-update warningsMove the geometry reading outside the animation, or use Layout
LazyVStack for ten itemsLazy machinery costs more than the itemsUse plain VStack
Grid for unbounded scroll contentEager; lays out everythingLazyVGrid
.fixedSize() blanket-appliedDisables wrapping; clips off-screenUse the per-axis form
Multi-line text truncated unexpectedlyParent proposed too little vertical space.fixedSize(horizontal: false, vertical: true)
frame(width: UIScreen.main.bounds.width)Ignores multitasking, iPad split view, macOS windowsUse proposal/containerRelativeFrame
Reading geometry to drive @State writes inside bodyLayout/update cycle warningUse onGeometryChange (iOS 18) or PreferenceKey

Gives 0 of the 12 instructions most ui components skills give

Counted across 343 of the 344 authors here whose files we hold, read 2026-08-06

  • Make touch targets at least 44x44 pixelsin 48 of 343, across 17 files
  • Use SVG icons instead of emojisin 39 of 343, across 12 files
  • Ensure minimum color contrast of 4.5:1in 36 of 343, across 9 files
  • provide visible focus rings on interactive elementsin 26 of 343, across 10 files
  • Use semantic color tokens instead of raw hex codesin 26 of 343, across 10 files
  • Generate a design system before codingin 23 of 343, across 6 files
  • Respect prefers-reduced-motion user settingsin 22 of 343, across 6 files
  • use semantic html elementsin 21 of 343, across 14 files
  • use semantic tailwind colorsin 19 of 343, across 6 files
  • Match style to product type and industryin 18 of 343, across 2 files
  • use consistent design tokensin 18 of 343, across 6 files
  • build complex interfaces from composable primitivesin 18 of 343, across 6 files

Said here and by no other author read

  • Memorize the two-pass parent-child layout negotiation model
  • Apply `.fixedSize` to prevent unwanted text truncation
  • Order view modifiers deliberately as nested wrappers
  • Use `containerRelativeFrame` for container-relative sizing
  • Isolate `GeometryReader` usage to avoid distorting layout
  • Use `.safeAreaInset` to push content away from edge views

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.