axiom-ux-flow-audit
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):
- Dead-End Views (CRITICAL)
- Dismiss Traps (CRITICAL)
- Buried CTAs (HIGH)
- Promise-Scope Mismatch (HIGH)
- Deep Link Dead Ends (HIGH)
- Missing Empty States (HIGH)
- Missing Loading/Error States (HIGH)
- Accessibility Dead Ends (HIGH)
3 Contextual Checks (check when product context warrants — these involve design judgment):
- Onboarding Gaps (MEDIUM) — requires knowing the product's onboarding strategy
- Broken Data Paths (MEDIUM) — overlaps with code correctness; include only when UX-visible
- 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, noaddTarget, no navigation push/present calls, noUIBarButtonItem - 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:
.fullScreenCoverwithout@Environment(\.dismiss)or explicit dismiss button;.sheetwith.interactiveDismissDisabled(true)without alternative dismiss; alert/confirmation dialogs missing cancel actions - UIKit:
present(_:animated:)withmodalPresentationStyle = .fullScreenwhere presented VC has no dismiss/close button;isModalInPresentation = truewithout 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
ScrollViewcontent - Important actions only in
.toolbaroverflow menu (.secondaryAction) - CTAs inside expandable
DisclosureGroupsections - 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 YNavigationLink("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:
.onOpenURLhandlers 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:
ListorForEachwithoutif items.isEmpty { ... }or.overlayfor empty state@Queryresults 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 { }orTask { }that fetches data without a loading indicator;try awaitwithout error presentation (no.alert, no error state variable); state enum missing.loadingor.errorcases - UIKit:
URLSessioncalls withoutUIActivityIndicatorViewor progress UI; completion handlers that don't update UI on error; missingUIAlertControllerfor failure cases - Both: Network calls without timeout or retry UI
- Both:
catchblocks that onlyprint/log in#if DEBUGwith 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/DragGesturewithout.accessibilityActionequivalent- Custom controls without
.accessibilityLabelor.accessibilityHint - Navigation that depends on color alone (e.g., "tap the green button")
- Pull-to-refresh (
refreshable) without VoiceOver-accessible alternative (note:refreshableis 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
@Bindingthat are initialized with.constant()in non-preview code - Views expecting
@Environmentvalues not provided by ancestors @Observablemodels created locally when they should be injected@Stateused where@Bindingshould 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:
NavigationStackwithoutNavigationSplitViewalternative for iPad- No
.horizontalSizeClasschecks 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:
@mainApp struct / SceneDelegate.onOpenURLhandlers (deep links)- Widget
Linkdestinations - Notification response handlers (
UNUserNotificationCenterDelegate) - Spotlight/Siri intent handlers
Step 2: Map Navigation Containers
Find all navigation structure:
NavigationStack/NavigationSplitViewTabViewwith tab structure.sheet/.fullScreenCoverpresentations- Custom modal presentations
Step 3: Trace Flows
For each entry point → completion path:
- Can the user reach their goal?
- Can the user get back?
- Does the user know what's happening at each step?
Step 4: Check Data Wiring
- Are
@Bindingvars actually passed from parent? - Are
@Observableobjects injected via environment? - Are
@Queryresults 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)