---
name: jpoindexter/ios-layout-and-grid
source: https://app.decimal.ai/s/jpoindexter-ios-layout-and-grid@1/SKILL.md
source_sha256: e387550b8d49
---

# iOS layout & grid — spacing that reads "designed", on iPhone

Source: Apple HIG **Layout**, UIKit/SwiftUI API docs, and the 8pt-grid convention (June 2026). Numbers tagged **[Apple]** (stated in HIG/API) or **[convention]** (community practice that *aligns with* Apple's point system — Apple publishes no mandatory pixel grid).

## 1. The 8-point grid (+ 4pt sub-grid) — [convention]
iOS measures in **points** (1pt = 1px @1x, 2px @2x, 3px @3x). Snap spacing/sizing to **multiples of 8** so they land on whole pixels at every scale (@2x→16, @3x→24) and scale cleanly across devices. Use a **4pt sub-grid** for finer/typographic spacing and baseline alignment.
- **Standard scale to use:** 4, 8, 12, 16, 24, 32, 40, 48.
- One consistent **gutter (16pt)** + one consistent **inter-card gap (8 or 12pt)** reads designed; mixing arbitrary gaps reads amateur.

## 2. Real layout numbers
- **Screen side margins:** `systemMinimumLayoutMargins` resolve to **16pt** on compact-width iPhones, **20pt** on larger widths. [Apple behavior / widely-cited] This is why inset-grouped lists and large titles share a left edge.
- **`directionalLayoutMargins`** — Apple's leading/trailing-aware default content spacing (use this, not left/right). [Apple]
- **`readableContentGuide`** — caps text width for readability; never exceeds the layout-margin guide and **widens with Dynamic Type**. Use for long body text. [Apple]
- **Bar heights — DON'T hard-code; read `safeAreaInsets`:** nav bar **44pt** (large-title adds a second row), tab bar **~49–50pt**, toolbar **44pt** (50 on iPad). Status bar / Dynamic Island top inset is device-dependent. Apple's rule: read `safeAreaInsets.top`/`.bottom`. [Apple]
- **`viewRespectsSystemMinimumLayoutMargins`** (default true) keeps root margins ≥ the system minimums. [Apple]

## 3. Touch targets — [Apple]
- **Design to 44×44 pt** minimum tappable area for every control — the long-standing, still-current rule, reaffirmed for iOS 26.
- Apple's Accessibility page now also lists a **28×28 pt absolute minimum** (44 = the recommended default). Treat 44 as the design target; 28 is a hard floor, not a goal.
- **Inline text links** are the explicit exception (allowed smaller — body line-height is shorter).
- **Gap between controls:** Apple states no single number; convention is **≥8pt** between adjacent 44pt targets (HIG also notes ~12pt around bezeled, ~24pt around un-bezeled controls).
- Density comes from tightening **non-interactive** content (text, icons, dividers have no minimum) — never from shrinking buttons below 44pt.

## 4. Dense, low-scroll layouts with SwiftUI grids
- **`LazyVGrid`/`LazyHGrid`** — lazy, renders only visible cells; for long/scrolling collections.
- **`Grid`/`GridRow`** (iOS 16+) — eager, for small fixed 2D layouts where you want column alignment across rows.
- **`GridItem` controls columns:**
  - `.fixed(w)` — exact-width column.
  - `.flexible(min:max:)` — splits width equally; use for explicit **2-up/3-up** card grids: `Array(repeating: .flexible(), count: 2)`.
  - `.adaptive(minimum:)` — fits as many columns as possible at a min width; for variable counts (chips, photo grids).
  - `GridItem(spacing:)` = inter-column gap; the grid's `spacing:` = row gap.
- **2-up vs 3-up rule of thumb [convention]:** on a 390pt phone (after 16pt margins + 16pt gutter), 2-up cards ≈ 170pt wide (label+value, still readable); 3-up ≈ 110pt (compact icon/number tiles only). Below ~100pt wide, switch to a **horizontal scroll row** instead of cramming a grid.

## 5. SwiftUI default spacing — set it explicitly
`VStack`/`HStack` with `spacing: nil` (the default) is **adaptive** — SwiftUI picks a distance per pair of subviews by type/platform; the "8pt default" is an approximation, **not a constant**. For a strict 8pt grid, **set spacing explicitly** (`VStack(spacing: 8)`). [Apple behavior]
- **Lists/Forms:** `listRowInsets` sets per-row padding; inset-grouped horizontal insets come from the table's `layoutMargins` (the 16/20pt system margins) — which is why inset-grouped cards align with large-title text. [Apple]

## 6. iOS 26 floating-chrome layout model
Nav bar + toolbars + tab bar render as floating **Liquid Glass** elements and **content scrolls underneath them** (vs old opaque bars that shrank the content frame). [Apple]
- Design so the top/bottom of scroll content can pass under translucent chrome **without permanently occluding important info** — the system adds bottom content insets so the last item clears the floating tab bar at rest.
- The tab bar is a floating, centered pill inset from the edges; don't force it opaque (reserves a dead inset — the "white bar" bug). See `ios-liquid-glass-swiftui`.

## 7. Greg application
- Lock the spacing scale to 4/8/12/16/24/32; `GregCard` padding to a grid multiple; one 16pt screen gutter, one consistent inter-card gap.
- Use a **2-up `LazyVGrid`** for the macro tiles (protein/carbs/fat/cost) instead of stacking full-width cards — cuts vertical scroll, each tile ≈170pt.
- Read `safeAreaInsets` for any custom bottom affordance; never hard-code a bottom pad above the tab bar.
- Set `VStack(spacing:)` explicitly everywhere (don't rely on the adaptive default) so the rhythm is intentional.
- Keep every tap target ≥44pt; gain density by tightening text/dividers and going multi-column, not by shrinking controls.

Pairs with: `ios-typography` (type rhythm), `ios-liquid-glass-swiftui` (floating chrome), `dense-no-scroll-layout` (cross-platform density), `ios-components` (bar heights/insets).