Accessibility

Accessibility is treated as a correctness requirement here, not a finishing pass. This is what the app does and how to verify it — a checklist you can copy into your own project.

VoiceOver

Dynamic Type

All text uses DesignSystem.Font.*, which maps to the system text styles (.body, .headline, .title, …) and scales with the user’s preferred size automatically.

Text that scales still needs room to land. At accessibility sizes a single word such as “Silent” is wider than a 110 pt card, so:

Reduce Motion

The only continuous animation in the app is the skeleton shimmer. SkeletonView reads @Environment(\.accessibilityReduceMotion) and, when it is on, renders a static dimmed block instead of the pulsing phaseAnimator. There are no repeatForever animations anywhere — that is both a Reduce Motion courtesy and the fix for a real AsyncRenderer crash (see the commit history).

Reduce Transparency

The trailer’s close button is the one place a material (.ultraThinMaterial) is used directly. It reads @Environment(\.accessibilityReduceTransparency) and swaps to an opaque DesignSystem.Color.cardBackground when the setting is on. The system navigation and tab bars handle their own translucency.

Hit targets

Interactive controls meet the 44×44 pt minimum. The trailer close button and the detail screen’s store link are sized to DesignSystem.Size.Button.minimumTapTarget explicitly; list rows and cards are comfortably larger.

Localization

Accessibility strings are localized like every other string — hints, labels, and the loading announcement live in the per-module String Catalogs in English and German, never hard-coded. Price and date formatting is locale-aware through FormatStyle.

The automated audit

AccessibilityAuditTests runs Xcode’s accessibility audit (contrast, hit targets, clipped text, missing labels, Dynamic Type support) on every screen: Discover, a movie’s details, the trailer, empty favorites, search, and search results. It runs once at the default text size and once at the largest accessibility size, and fails on any issue.

The audit reads pixels, so it also judges what nobody can see. A row scrolled beneath the floating tab bar reads as low contrast, and a carousel card peeking past the screen edge reads as clipped. The test counts an issue only when its element is fully on screen, outside a cut-off card, and clear of system chrome (the tab bar, the search field, the keyboard), which the app doesn’t draw.

Its first runs found five real problems, all fixed: the store link’s 18 pt hit area, truncated card titles and captions, a truncated empty-state title, an empty-state description that only nearly passed contrast, and a price VoiceOver read as a bare “$14.99” with no context. The last one surfaced only on CI’s older Xcode, whose audit checks differ from newer ones; the test names the failing element, so a CI-only failure is traceable from the log.

CI runs the audit on iPhone. Run locally on iPad, it found the squeezed cards above, which on iPhone sit off screen until you scroll. It also reports iPad-only system elements, such as the floating keyboard suggestion bar, so the iPad run is a manual check rather than a gate.

Two things it can’t judge. A word broken in the middle still counts as fitting, so the card widths are held by DesignTokenTests instead. And no audit checks that VoiceOver’s reading order makes sense, so that stays a manual pass.

How to verify