Summary
What remains of the VoiceOver / Dynamic Type debt after #1096. The wallet-backup blocker is tracked separately in #1097 because it needs a product decision first.
Baseline after #1096: 658 findings (scripts/a11y_audit.py --summary). The CI ratchet from #1091 stops this growing; this issue is about paying it down.
1. Security sliders are unusable — P0
DashWallet/Sources/UI/Menu/Security/Advanced Security/Views/DWSegmentSlider.m contains zero accessibility code. These are the auto-lock timer and the spending limit. A custom slider with no .adjustable trait does not exist for VoiceOver at all — a blind user cannot change either setting.
This is the only remaining P0 outside #1097. Not a one-liner: it needs accessibilityIncrement/accessibilityDecrement and a spoken value.
2. Nothing is ever announced
UIAccessibility.post has 0 uses app-wide, as does UIAccessibility.is*. Nothing tells a blind user that a copy succeeded, a PIN was wrong, a payment was sent, or that a quote expired mid-purchase. Copy feedback in particular is haptic + a visual HUD only (UIView+DWHUD.m).
3. Dynamic Type is actively defeated — worth its own effort
DashWallet/Sources/UI/SwiftUI Components/Font+DWStyle.swift:20-69 declares an extension Font whose members shadow Apple's own text styles — body, headline, largeTitle, title2, title3, callout, footnote, caption2 — as frozen .system(size:) values.
An app-module extension legally shadows the framework property, compiles without warning, and wins inside the module. So ~135 call sites that write .font(.body) and look entirely correct do not scale with the user's text size. There are 554 direct .system(size:) calls and zero uses of relativeTo: anywhere.
A low-vision user who raises text size in iOS Settings sees no change in the SwiftUI UI. The 2019 ObjC screens scale correctly, so the SwiftUI rewrite made this worse.
Fixing the tokens is ~4-6 hours; the layout QA that follows is 30-50, because every SwiftUI screen was built to frozen line heights. Probably its own ticket once someone picks it up.
4. Composite rows read as fragments
Contact rows, transaction cells and merchant rows announce as piles of disconnected pieces — .accessibilityElement(children: .combine) has only 7 uses app-wide. Worst offenders noted in the audit: ContactsScreen.swift (tappable rows via .onTapGesture, no button trait), PaymentSuccessHeader, the merchant/ATM list cells.
5. Interface Builder
8 image-only controls across the 38 .xib/.storyboard files have no accessibility configuration — including the Payments-sheet close button (Payments.storyboard:40) and the sync retry button (SyncView.xib:65). Only 3 IB files carry any accessibility markup at all, and all of it is traits without labels.
6. Known blind spots in the tooling
The static rules under-report, so 658 is a floor, not a measurement. Two confirmed misses, both real defects found by reading:
Anyone extending scripts/a11y_audit.py should start here: a Button whose label is a property reference rather than a literal.
Suggested order
DWSegmentSlider (P0, blocks a real setting)
- Announcements for copy / wrong PIN / payment sent
- Composite rows on the main flows
- Dynamic Type as a separate ticket
- IB files, opportunistically when those screens are touched
Context
Summary
What remains of the VoiceOver / Dynamic Type debt after #1096. The wallet-backup blocker is tracked separately in #1097 because it needs a product decision first.
Baseline after #1096: 658 findings (
scripts/a11y_audit.py --summary). The CI ratchet from #1091 stops this growing; this issue is about paying it down.1. Security sliders are unusable — P0
DashWallet/Sources/UI/Menu/Security/Advanced Security/Views/DWSegmentSlider.mcontains zero accessibility code. These are the auto-lock timer and the spending limit. A custom slider with no.adjustabletrait does not exist for VoiceOver at all — a blind user cannot change either setting.This is the only remaining P0 outside #1097. Not a one-liner: it needs
accessibilityIncrement/accessibilityDecrementand a spoken value.2. Nothing is ever announced
UIAccessibility.posthas 0 uses app-wide, as doesUIAccessibility.is*. Nothing tells a blind user that a copy succeeded, a PIN was wrong, a payment was sent, or that a quote expired mid-purchase. Copy feedback in particular is haptic + a visual HUD only (UIView+DWHUD.m).3. Dynamic Type is actively defeated — worth its own effort
DashWallet/Sources/UI/SwiftUI Components/Font+DWStyle.swift:20-69declares anextension Fontwhose members shadow Apple's own text styles —body,headline,largeTitle,title2,title3,callout,footnote,caption2— as frozen.system(size:)values.An app-module extension legally shadows the framework property, compiles without warning, and wins inside the module. So ~135 call sites that write
.font(.body)and look entirely correct do not scale with the user's text size. There are 554 direct.system(size:)calls and zero uses ofrelativeTo:anywhere.A low-vision user who raises text size in iOS Settings sees no change in the SwiftUI UI. The 2019 ObjC screens scale correctly, so the SwiftUI rewrite made this worse.
Fixing the tokens is ~4-6 hours; the layout QA that follows is 30-50, because every SwiftUI screen was built to frozen line heights. Probably its own ticket once someone picks it up.
4. Composite rows read as fragments
Contact rows, transaction cells and merchant rows announce as piles of disconnected pieces —
.accessibilityElement(children: .combine)has only 7 uses app-wide. Worst offenders noted in the audit:ContactsScreen.swift(tappable rows via.onTapGesture, no button trait),PaymentSuccessHeader, the merchant/ATM list cells.5. Interface Builder
8 image-only controls across the 38
.xib/.storyboardfiles have no accessibility configuration — including the Payments-sheet close button (Payments.storyboard:40) and the sync retry button (SyncView.xib:65). Only 3 IB files carry any accessibility markup at all, and all of it is traits without labels.6. Known blind spots in the tooling
The static rules under-report, so 658 is a floor, not a measurement. Two confirmed misses, both real defects found by reading:
NavigationBarElement.button(action:)— its label is theiconproperty, not a literalImage, so the rule saw no bare image (fixed in fix(ui): label the shared controls VoiceOver could not announce #1096)Anyone extending
scripts/a11y_audit.pyshould start here: aButtonwhose label is a property reference rather than a literal.Suggested order
DWSegmentSlider(P0, blocks a real setting)Context
AUDIT-SUMMARY.md)UI/SwiftUI Components/DashStepper.swift— label, value, hint and custom actions