Files
Matthias a60a76b797 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.
2026-04-19 21:11:32 +02:00

301 lines
14 KiB
Markdown

---
name: axiom-ux-flow-audit
description: Use when auditing user journeys, checking for UX dead ends, dismiss traps, buried CTAs, missing empty/loading/error states, or broken data paths in iOS apps (SwiftUI and UIKit).
license: MIT
---
# UX Flow Audit
**UX issues are not polish — they're defects that cause support tickets, bad reviews, and user churn.**
Axiom's code-level auditors check patterns. This skill checks what users actually experience: Can they complete their task? Can they get back? Do they know what's happening?
## 6 iOS UX Principles (Detection Anchors)
These principles anchor every detection category. When a principle is violated, users get stuck, confused, or frustrated.
### 1. Honor the Promise
What the button/title says must match what the user gets. A "Settings" button that opens a profile page breaks trust.
### 2. Escape Hatch
Every modal (sheet, fullScreenCover, alert) must have a way out. A sheet without a dismiss button or drag-to-dismiss traps users.
### 3. Primary Action Visibility
The main thing users came to do must be immediately visible and tappable. If the CTA requires scrolling or menu-diving, users won't find it.
### 4. Dead End Prevention
Every view must have a forward path (next step) or a completion state (success message, return to start). A view with no actions and no navigation is a dead end.
### 5. Progressive Disclosure
Don't overwhelm on first screen. Show essentials first, details on demand. An onboarding flow that dumps 12 settings on page one loses users.
### 6. Feedback Loop
Users must know what's happening during async operations. No loading state = "is it broken?" No error state = "what went wrong?" No empty state = "is this feature missing?"
## Detection Categories
**8 Core Defects** (always check — these are UX bugs, not opinions):
1. Dead-End Views (CRITICAL)
2. Dismiss Traps (CRITICAL)
3. Buried CTAs (HIGH)
4. Promise-Scope Mismatch (HIGH)
5. Deep Link Dead Ends (HIGH)
6. Missing Empty States (HIGH)
7. Missing Loading/Error States (HIGH)
8. Accessibility Dead Ends (HIGH)
**3 Contextual Checks** (check when product context warrants — these involve design judgment):
9. Onboarding Gaps (MEDIUM) — requires knowing the product's onboarding strategy
10. Broken Data Paths (MEDIUM) — overlaps with code correctness; include only when UX-visible
11. Platform Parity Gaps (MEDIUM) — depends on target device strategy
Core defects are always worth reporting. Contextual checks require product knowledge — flag them if they look wrong, but acknowledge they may be intentional decisions.
### 1. Dead-End Views (CRITICAL)
Views with no navigation forward, no actions, and no completion state.
**Detect**:
- SwiftUI: Views with no `NavigationLink`, `Button`, `.sheet`, `.fullScreenCover`, `.navigationDestination`, or dismiss action
- UIKit: View controllers with no `IBAction`, no `addTarget`, no navigation push/present calls, no `UIBarButtonItem`
- Check for views/VCs that are navigation destinations but offer no way to proceed or return
**Common cause**: Placeholder views during development that ship to production.
### 2. Dismiss Traps (CRITICAL)
Sheets or fullScreenCover without a dismiss path.
**Detect**:
- SwiftUI: `.fullScreenCover` without `@Environment(\.dismiss)` or explicit dismiss button; `.sheet` with `.interactiveDismissDisabled(true)` without alternative dismiss; alert/confirmation dialogs missing cancel actions
- UIKit: `present(_:animated:)` with `modalPresentationStyle = .fullScreen` where presented VC has no dismiss/close button; `isModalInPresentation = true` without alternative dismiss path
**Why critical**: Users literally cannot leave the screen. The only escape is force-quitting the app.
### 3. Buried CTAs (HIGH)
Primary actions hidden below fold, in menus, or behind navigation.
**Detect**:
- Primary action buttons placed after long `ScrollView` content
- Important actions only in `.toolbar` overflow menu (`.secondaryAction`)
- CTAs inside expandable `DisclosureGroup` sections
- No prominent action on the main tab's root view
**Not a buried CTA**: Below-fold placement that is intentional — checkout confirmation ("review order then confirm"), terms acceptance ("read then agree"), or content that the user should see before acting. The test: is the below-fold placement serving the user (they need context first) or hurting them (they can't find the action)?
### 4. Promise-Scope Mismatch (HIGH)
NavigationTitle, button label, or tab name doesn't match the content.
**Detect**:
- `.navigationTitle("X")` where view content is clearly about Y
- `NavigationLink("Settings")` that navigates to a profile/account view
- Tab labels that don't match tab content
- Button text suggesting one action but performing another
### 5. Deep Link Dead Ends (HIGH)
URL opens but lands on empty or broken state.
**Detect**:
- `.onOpenURL` handlers that push a view without checking if data exists
- Deep link destinations that assume pre-loaded state
- Universal link handling that doesn't validate the entity ID
- No fallback when deep-linked content is unavailable
**Cross-reference**: `axiom-swiftui-nav` covers deep link architecture. This category checks the UX outcome.
### 6. Missing Empty States (HIGH)
Lists, grids, or content views with no data show blank screen.
**Detect**:
- `List` or `ForEach` without `if items.isEmpty { ... }` or `.overlay` for empty state
- `@Query` results displayed without empty check
- Search results with no "no results" view
- Filtered views that can reach zero items
### 7. Missing Loading/Error States (HIGH)
Async operations with no feedback.
**Detect**:
- SwiftUI: `.task { }` or `Task { }` that fetches data without a loading indicator; `try await` without error presentation (no `.alert`, no error state variable); state enum missing `.loading` or `.error` cases
- UIKit: `URLSession` calls without `UIActivityIndicatorView` or progress UI; completion handlers that don't update UI on error; missing `UIAlertController` for failure cases
- Both: Network calls without timeout or retry UI
- Both: `catch` blocks that only `print`/log in `#if DEBUG` with no user-visible feedback — the user sees the operation silently fail
**Focus on network/write operations**: Skip loading indicators for fast local reads (GRDB queries, UserDefaults, cached data) that complete in under 100ms — adding spinners to these creates visual flicker. Focus on network calls, database writes, and any operation that can meaningfully fail.
**Scan systematically**: When you find a silent-error pattern in one file (e.g., `catch { print(...) }` without user feedback), scan ALL similar files for the same pattern. A single catch-block issue usually indicates a codebase-wide habit.
### 8. Accessibility Dead Ends (HIGH)
Actions only reachable via gestures or visual cues, invisible to assistive technology.
**Detect**:
- `.onLongPressGesture` / `.swipeActions` / `DragGesture` without `.accessibilityAction` equivalent
- Custom controls without `.accessibilityLabel` or `.accessibilityHint`
- Navigation that depends on color alone (e.g., "tap the green button")
- Pull-to-refresh (`refreshable`) without VoiceOver-accessible alternative (note: `refreshable` is automatically accessible — check custom implementations)
**Cross-reference**: `axiom-accessibility-diag` covers full WCAG compliance. This category specifically checks UX flow reachability from assistive technology.
### 9. Onboarding Gaps (MEDIUM)
First-launch flow that's incomplete or overwhelming.
**Detect**:
- No `@AppStorage`-gated onboarding check
- Onboarding flow without skip/later option
- More than 5 onboarding screens
- Onboarding that requires account creation before showing app value
### 10. Broken Data Paths (MEDIUM)
State/binding wiring issues that manifest as UX problems (view shows stale data, edits don't save, view appears empty when data exists).
**Detect**:
- Views accepting `@Binding` that are initialized with `.constant()` in non-preview code
- Views expecting `@Environment` values not provided by ancestors
- `@Observable` models created locally when they should be injected
- `@State` used where `@Binding` should propagate changes upward
**Scope note**: This overlaps with general SwiftUI correctness (`axiom-swiftui-debugging`). Include findings here only when the broken data path causes a visible UX problem — blank screen, stale content, edits that don't persist. Skip compiler-level or crash-level issues that belong in code review.
### 11. Platform Parity Gaps (MEDIUM)
iPad sidebar missing, landscape broken, Mac Catalyst issues.
**Detect**:
- `NavigationStack` without `NavigationSplitView` alternative for iPad
- No `.horizontalSizeClass` checks for adaptive layout
- Views that break in landscape (fixed heights, no scroll)
- Missing keyboard shortcut support on iPad/Mac
## Audit Process
### Step 1: Map Entry Points
Find all ways users enter the app:
- `@main` App struct / SceneDelegate
- `.onOpenURL` handlers (deep links)
- Widget `Link` destinations
- Notification response handlers (`UNUserNotificationCenterDelegate`)
- Spotlight/Siri intent handlers
### Step 2: Map Navigation Containers
Find all navigation structure:
- `NavigationStack` / `NavigationSplitView`
- `TabView` with tab structure
- `.sheet` / `.fullScreenCover` presentations
- Custom modal presentations
### Step 3: Trace Flows
For each entry point → completion path:
1. Can the user reach their goal?
2. Can the user get back?
3. Does the user know what's happening at each step?
### Step 4: Check Data Wiring
- Are `@Binding` vars actually passed from parent?
- Are `@Observable` objects injected via environment?
- Are `@Query` results handled for empty case?
### Step 5: Check Platform Adaptivity
- iPad: Does sidebar/split view work?
- Landscape: Does layout adapt?
- Mac Catalyst/Designed for iPad: Do keyboard shortcuts exist?
### Step 6: Check Accessibility Flows
- Can VoiceOver users complete every flow?
- Are gesture-only actions backed by accessibility actions?
## Cross-Auditor Correlation
When findings overlap with other Axiom auditors, note the correlation and elevate severity:
| UX Finding | Overlapping Auditor | Compound Effect | Severity Bump |
|------------|-------------------|-----------------|---------------|
| Dead end + missing NavigationPath | swiftui-nav-auditor | Programmatic fix impossible | CRITICAL |
| Gesture-only action + no `.accessibilityAction` | accessibility-auditor | Dead end for VoiceOver users | CRITICAL |
| Missing loading state + unhandled async error | concurrency-auditor | Crash + no user feedback | CRITICAL |
| Missing empty state + @Query with no results | swiftdata-auditor | Blank screen after data migration | HIGH |
| Deep link dead end + no URL validation | swiftui-nav-auditor | Silent failure from external link | HIGH |
## Output Format
### Enhanced Rating Table (for CRITICAL and HIGH findings)
| Finding | Urgency | Blast Radius | Fix Effort | ROI |
|---------|---------|-------------|-----------|-----|
| Dead-end after payment | Ship-blocker | All users | 30 min | Critical |
| Missing empty state on search | Next release | Users who search | 15 min | High |
**Urgency**: Ship-blocker / Next release / Backlog
**Blast Radius**: All users / Specific flow / Edge case
**Fix Effort**: Time estimate for the fix
**ROI**: Computed from urgency x blast radius / effort
### Navigation Reachability Score
At end of audit, output:
```
## Navigation Reachability
- Total screens found: [N] (views with navigation presentation)
- Deep-linkable screens: [N] (.onOpenURL can reach them)
- Widget-reachable screens: [N] (widget Link destinations)
- Notification-reachable screens: [N] (notification handlers)
- Coverage: [N]% of screens are externally reachable
```
## Fix Effort Reality Check
Most UX flow defects are fast fixes. When someone says "that's a big change," check this table:
| Defect | Typical Fix | Time |
|--------|------------|------|
| Dismiss trap (no close button) | Add toolbar Cancel button + `dismiss()` | 10-15 min |
| Missing empty state | Add `if items.isEmpty { ContentUnavailableView(...) }` | 15-20 min |
| Buried CTA (placement change) | Move button from `.secondaryAction` to `.primaryAction` | 20-30 min |
| Dead-end view (no forward path) | Add NavigationLink or action button | 15-30 min |
| Missing loading state | Add `@State var isLoading` + ProgressView overlay | 15-20 min |
| Silent error (no user feedback) | Add `.alert` presentation on catch block | 10-15 min |
| Gesture-only action | Add `.accessibilityAction` + visible button alternative | 15-20 min |
**The cost of NOT fixing**: A dismiss trap or dead end after payment generates 1-star reviews within hours of launch. Each review costs 10-20 positive reviews to offset. The 15-minute fix prevents weeks of damage control.
## Anti-Rationalization
| Thought | Reality |
|---------|---------|
| "UX issues are just polish, we'll fix later" | UX dead ends cause 1-star reviews. They're defects, not enhancements. A 15-min fix now prevents weeks of damage control. |
| "Users will figure it out" | Users don't figure it out. They delete the app. Average user tries for 30 seconds. |
| "We'll add empty states after launch" | Empty states are the FIRST thing new users see. Launching without them means launching broken. |
| "That fix is a big design change" | Most UX fixes are placement or state changes (10-30 min). Check the Fix Effort table above. |
| "Accessibility is a separate concern" | If VoiceOver users can't complete a flow, it's a dead end. Same defect, different user. |
| "This screen is just temporary" | Temporary screens ship. Check them anyway. |
| "The dismiss gesture handles it" | fullScreenCover has no dismiss gesture. That's the trap. |
## Resources
**Skills**: axiom-swiftui-nav, axiom-accessibility-diag, axiom-hig, axiom-swiftui-debugging
**Agents**: ux-flow-auditor (automated scanning), swiftui-nav-auditor (navigation architecture), accessibility-auditor (WCAG compliance)