Repository navigation
feat(a11y): Dynamic Type, VoiceOver, 44pt targets and Reduce Motion across the app - #41
Merged
Merged
Conversation
Every text in the app was a fixed .system(size:) — Dynamic Type did nothing. .warmFont(size, weight:, design:) renders at exactly the design size at the default content size (unit-tested as an identity across the scale) and follows the user's reading size from there, on the curve of the text style closest to that size so headings grow less than body at accessibility sizes. It is a ViewModifier that reads the size category from the environment, so views re-render on change instead of baking a stale Font value. .tappable44() enlarges a small control's hit area to the 44pt minimum without changing its look.
The mechanical half of the pass: 285 `.font(.system(size:…))` sites across the app become `.warmFont(…)` with the same size, weight and design — pixel-identical at the default content size, scaling from there. The two Text concatenations in AddGoalFlow (which need a Font value, not a modifier) scale through @ScaledMetric instead. Fixed widths that would clip at accessibility sizes now scale with the text: the inline amount fields on the Money tab and the two number fields in the goal edit sheet.
- Every icon-only control has a name: the "+" shell (with a hint), tab bar items (isTabBar container, isSelected on the active one), sheet back/close chevrons, the keypad's ⌫ and decimal point, the Money tab's inline "+" buttons, the Goals "+" buttons, the favorite star (label flips with state), the wizard's back/close, clear-name and month arrows. All of them get a ≥44pt hit area via tappable44(). - Rows read as one element with a spoken value: recent transactions, expense and income rows (with the category/source and a long-press hint), monthly summary cells, goal cards, achievement rows. Rings and bars expose "N percent". The keypad amount reads "Expense amount, 500 dollars" instead of three fragments; the income trend badge reads "Month over month, up 12 percent". Decorative tiles/stars/glyphs are hidden from VoiceOver. - honorsReduceMotion(): under Reduce Motion every animation in a subtree becomes instant. Applied at the app root, the quick-add sheet, the goal wizard cover, the goal edit sheet and the deposit sheet — the presented roots the app-level modifier cannot reach. - Every new spoken string is localized: literals go through the String Catalog, interpolations through String(localized:) / Text so the key is a real format string; es-419 added for all 37 new keys.
At accessibility sizes the four tab labels wrapped to single letters and the keypad glyphs outgrew their 52pt cells. UITabBar and the system number pad do not scale into the accessibility range either — they rely on VoiceOver labels and hit targets — so both cap at xxxLarge. gotchas.yaml and DESIGN.md record the rules this pass introduced: warmFont over .system(size:), tappable44, labels on icon-only controls, honorsReduceMotion on presented roots, localized spoken strings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Executes
docs/plans/pr-d3-accessibility.md— the app-wide VoiceOver / Dynamic Type / touch-target pass behind the WCAG AA commitment inPRODUCT.md. The default look does not change (unit-tested identity at the default content size).f78dae9,c970f1b):.warmFont(size, weight:, design:)inFont+Warm.swiftrenders exactly the design size at the default content size and scales on the curve of the nearest text style (headings grow less than body at AX sizes). It is aViewModifierthat reads the size category, so views re-render on change instead of baking a staleFont. 285 fixed.system(size:)sites converted; the twoTextconcatenations in the wizard scale through@ScaledMetric. Fixed widths that would clip (Money inline amount fields, edit-sheet number fields) now scale with the text.WarmTypeTests: identity across the whole size table, AX scaling up / small down, heading curve < body curve, style mapping.529ec8b): every icon-only control has a name — "+" shell (with hint), tab items (.isTabBarcontainer,.isSelected), sheet back/close chevrons, keypad ⌫ / decimal point, inline "+" buttons, Goals "+", favorite star (label flips), wizard back/close/clear/month arrows. Rows read as one element with a value: recent transactions, expense/income rows (category/source + long-press hint), monthly cells, goal cards, achievements. Rings/bars expose "N percent"; the keypad amount reads "Expense amount, 500 dollars"; the trend badge reads "Month over month, up 12 percent". Decorative tiles/glyphs hidden. 0Toggle("")left (the last two, in the wizard, were fixed in C)..tappable44()on every sub-44 control (32/36/40pt visuals unchanged; hit area grows)..honorsReduceMotion()makes every animation in a subtree instant; applied at the app root and at each presented root (quick-add sheet, goal wizard cover, edit sheet, deposit sheet), since sheets are their own tree.Text/String(localized:)so the key is a real format string. es-419 added for all 37 new keys.e15e15a): at AX3 the four tab labels wrapped to single letters — the tab bar and the numeric keypad now cap atxxxLarge, exactly whatUITabBar/ the system number pad do; VoiceOver labels + 44pt targets carry accessibility there.gotchas.yaml+DESIGN.mdrecord the rules (warmFont, tappable44, labels, honorsReduceMotion, capping).Numbers
.system(size:)@ScaledMetricconcatenations, +1 inside the modifier)accessibilityLabelaccessibilityHidden(decorative)Toggle("")tappable44×14, rest were already ≥44)Test plan
xcodebuild build— BUILD SUCCEEDEDxcodebuild test -skip-testing:SavelyUITests— TEST SUCCEEDED (+4WarmTypeTests)swiftlint lint --strict— 0accessibility-extra-extra-extra-large) screenshot of the Dashboard viasimctl ui content_size: tab bar labels intact after the cap; text scalesRisks
.warmFontreturnssome View(wasText) — compiled clean everywhere; the only Text-returning need was the two concatenations, handled..tappable44()grows a control's layout frame to 44pt when its visual is smaller — a few HStacks gain 2–6pt around a chevron/star. Nothing perceptible in the AX3 shot, worth an eyeball at default size..honorsReduceMotion()disables all animation under Reduce Motion (instant, not crossfade). The plan asked for instant.Checklist
feat/branch, Conventional Commits, no AI attributiongotchas.yaml/DESIGN.mdupdated with the new invariant