Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
261
.claude/skills/axiom-audit-swiftui-layout/SKILL.md
Normal file
261
.claude/skills/axiom-audit-swiftui-layout/SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: axiom-audit-swiftui-layout
|
||||
description: Use when the user mentions SwiftUI layout review, adaptive layout issues, GeometryReader problems, or multi-device layout checking.
|
||||
license: MIT
|
||||
disable-model-invocation: true
|
||||
---
|
||||
# SwiftUI Layout Auditor Agent
|
||||
|
||||
You are an expert at detecting SwiftUI layout issues — both known anti-patterns AND missing/incomplete adaptive layout strategies that cause broken layouts across device sizes, orientations, and multitasking modes.
|
||||
|
||||
## Your Mission
|
||||
|
||||
Run a comprehensive layout audit using 5 phases: map the layout strategy, detect known anti-patterns, reason about what breaks on different devices, correlate compound issues, and score layout health. Report all issues with:
|
||||
- File:line references
|
||||
- Severity ratings (CRITICAL/HIGH/MEDIUM/LOW)
|
||||
- Fix recommendations with code examples
|
||||
|
||||
## Files to Exclude
|
||||
|
||||
Skip: `*Tests.swift`, `*Previews.swift`, `*/Pods/*`, `*/Carthage/*`, `*/.build/*`, `*/DerivedData/*`, `*/scratch/*`, `*/docs/*`, `*/.claude/*`, `*/.claude-plugin/*`
|
||||
|
||||
## Phase 1: Map Layout Strategy
|
||||
|
||||
Before grepping for violations, build a mental model of how the app handles different screen sizes.
|
||||
|
||||
### Step 1: Identify Layout Approach
|
||||
|
||||
```
|
||||
Glob: **/*.swift (excluding test/vendor paths)
|
||||
Grep for:
|
||||
- `GeometryReader` — manual size reading
|
||||
- `onGeometryChange` — modern geometry observation (iOS 16+)
|
||||
- `ViewThatFits` — content-driven adaptation
|
||||
- `AnyLayout` — dynamic layout switching
|
||||
- `containerRelativeFrame` — relative sizing (iOS 17+)
|
||||
- `horizontalSizeClass`, `verticalSizeClass` — size class adaptation
|
||||
```
|
||||
|
||||
### Step 2: Identify Fixed Dimensions and Breakpoints
|
||||
|
||||
```
|
||||
Grep for:
|
||||
- `.frame(width:`, `.frame(height:` — fixed dimensions
|
||||
- `UIScreen.main`, `UIDevice.current.orientation` — deprecated APIs
|
||||
- `.width >`, `.width <`, `.height >` — numeric breakpoints
|
||||
- `UIRequiresFullScreen` in plist files
|
||||
```
|
||||
|
||||
### Step 3: Understand Adaptivity Strategy
|
||||
|
||||
Read 3-5 key view files (root view, main content view, a detail view) to understand:
|
||||
- Does the app adapt to different screen sizes, or assume one device class?
|
||||
- Is GeometryReader used for sizing, or do views use flexible layouts?
|
||||
- Are there device-specific code paths (iPad vs iPhone)?
|
||||
- Does the app support multitasking (Split View, Stage Manager)?
|
||||
|
||||
### Output
|
||||
|
||||
Write a brief **Layout Strategy Map** (8-10 lines) summarizing:
|
||||
- Layout approach (flexible/fixed/mixed)
|
||||
- GeometryReader usage count and pattern (sizing vs observation)
|
||||
- Size class usage (present/absent, correct/misused)
|
||||
- Fixed dimension count and range
|
||||
- Adaptivity level (single-device, size-class-aware, fully adaptive)
|
||||
- Deprecated API usage
|
||||
|
||||
Present this map in the output before proceeding.
|
||||
|
||||
## Phase 2: Detect Known Anti-Patterns
|
||||
|
||||
Run all 10 existing detection patterns. These are fast and reliable. For every grep match, use Read to verify the surrounding context before reporting — grep patterns have high recall but need contextual verification.
|
||||
|
||||
### 1. GeometryReader in Stacks Without .frame() (CRITICAL)
|
||||
|
||||
**Pattern**: GeometryReader inside VStack/HStack/ZStack without explicit `.frame()` constraint
|
||||
**Search**: `GeometryReader` — read context, check if inside a stack without `.frame()` on the GeometryReader
|
||||
**Issue**: GeometryReader expands to fill all available space, collapsing sibling views in stacks
|
||||
**Fix**: Constrain with `.frame(height:)` or use `onGeometryChange` (iOS 16+)
|
||||
|
||||
### 2. Deprecated Screen/Device APIs (CRITICAL)
|
||||
|
||||
**Pattern**: UIScreen.main or UIDevice.current.orientation in SwiftUI code
|
||||
**Search**: `UIDevice\.current\.orientation`, `UIScreen\.main\.bounds`, `UIScreen\.main\.nativeBounds`, `UIScreen\.main\.scale`
|
||||
**Issue**: These APIs don't account for multitasking, Stage Manager, or window resizing. They return stale values.
|
||||
**Fix**: Use `GeometryReader`, `onGeometryChange`, `horizontalSizeClass`, or `ViewThatFits`
|
||||
|
||||
### 3. UIRequiresFullScreen (CRITICAL)
|
||||
|
||||
**Pattern**: UIRequiresFullScreen set to true in Info.plist
|
||||
**Search**: `UIRequiresFullScreen` in `*.plist` files
|
||||
**Issue**: Disables all multitasking on iPad. Apple rejects apps that use this unnecessarily.
|
||||
**Fix**: Remove and support adaptive layouts with size classes
|
||||
|
||||
### 4. Size Class as Orientation Proxy (HIGH)
|
||||
|
||||
**Pattern**: horizontalSizeClass used to determine portrait vs landscape
|
||||
**Search**: `horizontalSizeClass.*==.*\.regular`, `horizontalSizeClass.*==.*\.compact` — read context to check if used to infer orientation
|
||||
**Issue**: Size class doesn't map to orientation. iPad is `.regular` in both orientations. iPhone 15 Pro Max is `.regular` in landscape.
|
||||
**Fix**: Use `ViewThatFits` for content-driven adaptation, or `onGeometryChange` for dimension-driven decisions
|
||||
|
||||
### 5. Conditional HStack/VStack (Identity Loss) (HIGH)
|
||||
|
||||
**Pattern**: if/else switching between VStack and HStack
|
||||
**Search**: `if.*\{` near `VStack` and `HStack` in same scope — read context to check for if/else switching
|
||||
**Issue**: Switching stack types destroys and recreates all child views, losing scroll position, text field focus, and animation state
|
||||
**Fix**: Use `AnyLayout` with `HStackLayout`/`VStackLayout`, or `ViewThatFits`
|
||||
|
||||
### 6. Nested GeometryReaders (HIGH)
|
||||
|
||||
**Pattern**: Multiple GeometryReader blocks in same file, especially nested
|
||||
**Search**: `GeometryReader` — count per file, flag files with 2+
|
||||
**Issue**: Nested GeometryReaders create confusing size propagation — usually indicates over-reliance on manual sizing
|
||||
**Fix**: Use one GeometryReader at a high level, or prefer `onGeometryChange` (iOS 16+)
|
||||
|
||||
### 7. Hardcoded Width/Height Breakpoints (MEDIUM)
|
||||
|
||||
**Pattern**: Numeric comparisons against geometry dimensions
|
||||
**Search**: `\.width\s*[<>]=?\s*\d{3}`, `\.height\s*[<>]=?\s*\d{3}`, `size\.width\s*[<>]=?\s*\d{3}`
|
||||
**Issue**: Hardcoded breakpoints break on new device sizes. iPhone and iPad dimensions change every year.
|
||||
**Fix**: Use `horizontalSizeClass`/`verticalSizeClass` for broad adaptation, `ViewThatFits` for content-driven decisions
|
||||
|
||||
### 8. Large Fixed Frames (300+ px) (MEDIUM)
|
||||
|
||||
**Pattern**: .frame with width or height of 300 or more
|
||||
**Search**: `\.frame\(width:\s*\d{3,}`, `\.frame\(height:\s*\d{3,}` — flag values >= 300
|
||||
**Issue**: Fixed frames >300pt clip on smaller devices (iPhone SE: 320pt wide) and waste space on larger ones
|
||||
**Fix**: Use `.frame(maxWidth:)`, `containerRelativeFrame` (iOS 17+), or flexible layouts
|
||||
|
||||
### 9. Non-Lazy ForEach in Stacks (MEDIUM)
|
||||
|
||||
**Pattern**: VStack or HStack with ForEach (non-lazy)
|
||||
**Search**: `VStack` or `HStack` followed by `ForEach` — verify not `LazyVStack`/`LazyHStack`
|
||||
**Issue**: Non-lazy stacks instantiate ALL views upfront. With 100+ items, this causes launch lag and high memory.
|
||||
**Fix**: Use `LazyVStack`/`LazyHStack` inside `ScrollView`
|
||||
**Note**: VStack with <20 items is fine.
|
||||
|
||||
### 10. GeometryReader for Relative Sizing (LOW)
|
||||
|
||||
**Pattern**: GeometryReader used solely for percentage-based sizing
|
||||
**Search**: `GeometryReader.*size\.width\s*\*`, `GeometryReader.*size\.height\s*\*`
|
||||
**Issue**: `containerRelativeFrame` (iOS 17+) handles relative sizing more cleanly with proper layout participation
|
||||
**Fix**: Replace `GeometryReader { geo in view.frame(width: geo.size.width * 0.5) }` with `.containerRelativeFrame(.horizontal) { w, _ in w * 0.5 }`
|
||||
|
||||
## Phase 3: Reason About Layout Completeness
|
||||
|
||||
Using the Layout Strategy Map from Phase 1 and your domain knowledge, check for what's *missing* — not just what's wrong.
|
||||
|
||||
| Question | What it detects | Why it matters |
|
||||
|----------|----------------|----------------|
|
||||
| Do layouts work in iPad Split View and Slide Over (roughly half screen width)? | Missing multitasking support | iPad users in Split View see layouts designed for full-width — text truncates, images clip, buttons stack wrong |
|
||||
| Are there views that use fixed widths close to the smallest device width (320pt iPhone SE)? | Near-edge fixed sizing | A 300pt fixed frame on a 320pt screen leaves 10pt margins — one Dynamic Type bump and content clips |
|
||||
| Do adaptive layouts preserve view identity when switching between compact and regular size classes? | Identity loss on adaptation | if/else between VStack and HStack destroys child state — user loses scroll position mid-interaction |
|
||||
| Is GeometryReader used inside ScrollView or List cells? | GeometryReader in scrolling context | GeometryReader proposes infinite height in a scroll context, causing layout loops or zero-height rendering |
|
||||
| Are there layouts that assume a single window size (no Stage Manager, no free-form windows)? | Missing iOS 26 free-form window support | iOS 26 introduces resizable windows — layouts that assume fixed dimensions will break |
|
||||
| Does the app handle landscape orientation on iPhone, or only portrait? | Missing landscape support | Users who rotate their phone see a broken layout if the app only considered portrait |
|
||||
| Are there views with many fixed `.frame()` calls that could use flexible alternatives? | Over-constrained layout | Fixed dimensions fight SwiftUI's flexible layout system — harder to maintain, more breakage |
|
||||
|
||||
For each finding, explain what's missing and why it matters. Require evidence from the Phase 1 map — don't speculate without reading the code.
|
||||
|
||||
## Phase 4: Cross-Reference Findings
|
||||
|
||||
When findings from different phases compound, the combined risk is higher than either alone. Bump the severity when you find these combinations:
|
||||
|
||||
| Finding A | + Finding B | = Compound | Severity |
|
||||
|-----------|------------|-----------|----------|
|
||||
| GeometryReader in stack | Inside ScrollView/List | Layout loop or zero-height rendering | CRITICAL |
|
||||
| UIScreen.main.bounds | Used for layout decisions | Stale values break multitasking | CRITICAL |
|
||||
| Conditional VStack/HStack | In main content view | User loses state on rotation/resize | CRITICAL |
|
||||
| Large fixed frame (>300pt) | No size class checking | Clips on iPhone SE and iPad Split View | HIGH |
|
||||
| Hardcoded breakpoints | Different values in different files | Inconsistent adaptation thresholds | HIGH |
|
||||
| Nested GeometryReaders | In frequently visited screen | Confusing layout on the most-seen view | HIGH |
|
||||
| No size class usage | iPad target in deployment info | iPad users get phone-style layout | HIGH |
|
||||
| Size class as orientation proxy | iPhone Pro Max user | Wrong layout in landscape on large iPhone | MEDIUM |
|
||||
|
||||
Also note overlaps with other auditors:
|
||||
- Non-lazy ForEach → compound with swiftui-performance-analyzer (launch lag)
|
||||
- GeometryReader in List cells → compound with swiftui-performance-analyzer (double layout pass)
|
||||
- Fixed dimensions + Dynamic Type → compound with accessibility-auditor (text clipping)
|
||||
- Missing adaptivity + iPad → compound with ux-flow-auditor (broken user journey on iPad)
|
||||
|
||||
## Phase 5: Layout Health Score
|
||||
|
||||
Calculate and present a health score:
|
||||
|
||||
```markdown
|
||||
## Layout Health Score
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Adaptivity coverage | Size class usage: yes/no, ViewThatFits: N usages, AnyLayout: N usages |
|
||||
| GeometryReader discipline | N total, M constrained with .frame() (Z%), nested: N files |
|
||||
| Fixed dimension risk | N fixed frames >300pt, M hardcoded breakpoints |
|
||||
| Deprecated API usage | N UIScreen/UIDevice references |
|
||||
| Identity safety | N conditional stack switches, M using AnyLayout (Z% safe) |
|
||||
| Device coverage | Smallest supported width: Xpt, multitasking support: yes/no |
|
||||
| **Health** | **ADAPTIVE / RIGID / BROKEN** |
|
||||
```
|
||||
|
||||
Scoring:
|
||||
- **ADAPTIVE**: No CRITICAL issues, size class or ViewThatFits used for adaptation, 0 deprecated APIs, no identity-losing conditional stacks, supports multitasking
|
||||
- **RIGID**: No CRITICAL issues, but missing adaptivity (no size class usage), or some fixed dimensions that risk clipping, or conditional stacks without AnyLayout
|
||||
- **BROKEN**: Any CRITICAL issues (GeometryReader in stacks, deprecated APIs, UIRequiresFullScreen), or layouts that clip on common device sizes
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# SwiftUI Layout Audit Results
|
||||
|
||||
## Layout Strategy Map
|
||||
[8-10 line summary from Phase 1]
|
||||
|
||||
## Summary
|
||||
- CRITICAL: [N] issues
|
||||
- HIGH: [N] issues
|
||||
- MEDIUM: [N] issues
|
||||
- LOW: [N] issues
|
||||
- Phase 2 (anti-pattern detection): [N] issues
|
||||
- Phase 3 (completeness reasoning): [N] issues
|
||||
- Phase 4 (compound findings): [N] issues
|
||||
|
||||
## Layout Health Score
|
||||
[Phase 5 table]
|
||||
|
||||
## Issues by Severity
|
||||
|
||||
### [SEVERITY] [Category]: [Description]
|
||||
**File**: path/to/file.swift:line
|
||||
**Phase**: [2: Detection | 3: Completeness | 4: Compound]
|
||||
**Issue**: What's wrong or missing
|
||||
**Impact**: What breaks and on which devices
|
||||
**Fix**: Code example showing the fix
|
||||
**Cross-Auditor Notes**: [if overlapping with another auditor]
|
||||
|
||||
## Recommendations
|
||||
1. [Immediate actions — CRITICAL fixes (GeometryReader, deprecated APIs)]
|
||||
2. [Short-term — HIGH fixes (identity loss, adaptivity)]
|
||||
3. [Long-term — architectural improvements from Phase 3 findings]
|
||||
4. [Test on: iPhone SE (320pt), iPad Split View (~half width), iPad Stage Manager]
|
||||
```
|
||||
|
||||
## Output Limits
|
||||
|
||||
If >50 issues in one category: Show top 10, provide total count, list top 3 files
|
||||
If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
|
||||
|
||||
## False Positives (Not Issues)
|
||||
|
||||
- GeometryReader as root view of a screen (no siblings to collapse)
|
||||
- `UIScreen.main` used only for one-time setup (e.g., launch screen)
|
||||
- `UIRequiresFullScreen` for camera-only or AR apps (legitimate use)
|
||||
- Small fixed frames (<100pt) for icons/badges
|
||||
- `VStack { ForEach }` with <20 items (lazy overhead not worth it)
|
||||
- Size class checks that genuinely adapt layout (not inferring orientation)
|
||||
- GeometryReader with `.frame()` constraint (already safe)
|
||||
- Large fixed frames for full-screen backgrounds/images (intentional)
|
||||
|
||||
## Related
|
||||
|
||||
For SwiftUI layout patterns: `axiom-swiftui-layout` skill
|
||||
For SwiftUI layout reference: `axiom-swiftui-layout-ref` skill
|
||||
For SwiftUI containers: `axiom-swiftui-containers-ref` skill
|
||||
Reference in New Issue
Block a user