--- 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)