Commit 69c34c3ded
69c34c3ded7f112622aaaaad62b5a8a752ed3956
parent: 4cb642b1b7
Unsigned
cmc <hello@cleberg.net> · 2026-10-18 06:29 UTC
Accessibility audit and fixes (!19)
- New =MoonriseUITests= target: Xcode accessibility audit over every main screen; issues printed as =A11Y= lines.
- Fixes: wrapping instead of truncation (Launches, Library, Today summary), 44 pt tap targets, higher opacity on dimmed controls, combined fact rows, default tint in Settings.
| Issue type | Before | After |
|-
| Text clipped | 11 | 1 |
| Hit area too small | 9 | 0 |
| Contrast failed / nearly | 5 | 7 |
| Dynamic Type partially unsupported | 36 | 36 |
| Other | 2 | 2 |
Remaining contrast flags are white text under the translucent tab bar and the system Done button; Dynamic Type flags include system controls. =swift test= passes; UI test passes.
Layout: unified · split
App/Sources/LaunchesScreen.swift
+2 −2
| @@ -64,9 +64,9 @@ struct LaunchesScreen: View { |
| 64 | 64 | StatusTag(status: l.status, ink: ink, accent: accent) |
| 65 | 65 | } |
| 66 | 66 | Text(l.rocket).font(.title2.weight(.medium)) |
| 67 | | if let m = l.mission { Text(m).font(.subheadline).lineLimit(1) } |
| 67 | if let m = l.mission { Text(m).font(.subheadline) } |
| 68 | 68 | HStack { |
| 69 | | Text("\(l.provider) · \(l.location)").font(.caption2).opacity(0.6).lineLimit(1) |
| 69 | Text("\(l.provider) · \(l.location)").font(.caption2).opacity(0.7) |
| 70 | 70 | Spacer() |
| 71 | 71 | Countdown(to: l.net).font(.footnote.weight(.medium).monospacedDigit()) |
| 72 | 72 | } |
App/Sources/LibraryScreen.swift
+2 −1
| @@ -66,7 +66,7 @@ struct PageRow: View { |
| 66 | 66 | var body: some View { |
| 67 | 67 | VStack(alignment: .leading, spacing: 2) { |
| 68 | 68 | Text(page.title).font(.body) |
| 69 | | Text(page.subtitle).font(.caption).opacity(0.6).lineLimit(1) |
| 69 | Text(page.subtitle).font(.caption).opacity(0.7) |
| 70 | 70 | } |
| 71 | 71 | .padding(.vertical, 2) |
| 72 | 72 | } |
| @@ -111,6 +111,7 @@ struct LibraryPageView: View { |
| 111 | 111 | Text(f.label).font(.footnote).opacity(0.6) |
| 112 | 112 | Text(f.value).font(.subheadline.monospacedDigit()) |
| 113 | 113 | } |
| 114 | .accessibilityElement(children: .combine) |
| 114 | 115 | Divider().overlay(ink.opacity(0.15)) |
| 115 | 116 | } |
| 116 | 117 | } |
App/Sources/SkyScreen.swift
+1
| @@ -59,6 +59,7 @@ struct SkyScreen: View { |
| 59 | 59 | .sheet(isPresented: $showSettings) { |
| 60 | 60 | SettingsScreen { model.applyLocationSettings() } |
| 61 | 61 | .preferredColorScheme(.dark) |
| 62 | .tint(.blue) |
| 62 | 63 | } |
| 63 | 64 | .sheet(item: $libraryPage) { page in |
| 64 | 65 | NavigationStack { LibraryPageView(page: page) } |
App/Sources/SystemScreen.swift
+6 −4
| @@ -38,9 +38,11 @@ struct SystemScreen: View { |
| 38 | 38 | .font(.subheadline.weight(.medium).monospacedDigit()) |
| 39 | 39 | .frame(width: 80, alignment: .leading) |
| 40 | 40 | Slider(value: $offsetDays, in: range).tint(ink) |
| 41 | | Button("Today") { playing = false; offsetDays = 0 } |
| 42 | | .font(.footnote.weight(.medium)) |
| 43 | | .opacity(abs(offsetDays) < 1 ? 0.4 : 1) |
| 41 | Button { playing = false; offsetDays = 0 } label: { |
| 42 | Text("Today").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle()) |
| 43 | } |
| 44 | .font(.footnote.weight(.medium)) |
| 45 | .opacity(abs(offsetDays) < 1 ? 0.6 : 1) |
| 44 | 46 | } |
| 45 | 47 | .padding(.top, 12) |
| 46 | 48 | |
| @@ -58,7 +60,7 @@ struct SystemScreen: View { |
| 58 | 60 | ForEach(Spacecraft.all) { craft in |
| 59 | 61 | GridRow { |
| 60 | 62 | if let page = Library.page(spacecraft: craft.name) { |
| 61 | | Button { libraryPage = page } label: { Text(craft.name).underline() }.buttonStyle(.plain) |
| 63 | Button { libraryPage = page } label: { Text(craft.name).underline().frame(minHeight: 44, alignment: .leading).contentShape(Rectangle()) }.buttonStyle(.plain) |
| 62 | 64 | } else { |
| 63 | 65 | Text(craft.name) |
| 64 | 66 | } |
App/Sources/TodaySheet.swift
+8 −4
| @@ -121,10 +121,12 @@ struct TodayBar: View { |
| 121 | 121 | scrubber |
| 122 | 122 | Button(action: onExpand) { |
| 123 | 123 | HStack { |
| 124 | | Text(summary).font(.footnote).opacity(0.75).lineLimit(2) |
| 124 | Text(summary).font(.footnote).opacity(0.8) |
| 125 | 125 | Spacer() |
| 126 | 126 | Image(systemName: "chevron.up").font(.caption.weight(.medium)).opacity(0.6) |
| 127 | 127 | } |
| 128 | .frame(minHeight: 44) |
| 129 | .contentShape(Rectangle()) |
| 128 | 130 | } |
| 129 | 131 | .buttonStyle(.plain) |
| 130 | 132 | .accessibilityHint("Shows tonight's details") |
| @@ -157,9 +159,11 @@ struct TodayBar: View { |
| 157 | 159 | .font(.footnote.monospacedDigit()) |
| 158 | 160 | .frame(width: 70, alignment: .leading) |
| 159 | 161 | Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink) |
| 160 | | Button("Now") { model.offsetHours = 0 } |
| 161 | | .font(.footnote.weight(.medium)) |
| 162 | | .opacity(model.offsetHours == 0 ? 0.4 : 1) |
| 162 | Button { model.offsetHours = 0 } label: { |
| 163 | Text("Now").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle()) |
| 164 | } |
| 165 | .font(.footnote.weight(.medium)) |
| 166 | .opacity(model.offsetHours == 0 ? 0.6 : 1) |
| 163 | 167 | } |
| 164 | 168 | } |
| 165 | 169 | |
App/UITests/AccessibilityAuditTests.swift
added
+49
| @@ -0,0 +1,49 @@ |
| 1 | import XCTest |
| 2 | |
| 3 | /// Runs Xcode's accessibility audit on each main screen and prints every issue. |
| 4 | /// Issues are recorded, not failed, so one run reports everything. |
| 5 | final class AccessibilityAuditTests: XCTestCase { |
| 6 | var app: XCUIApplication! |
| 7 | var issues: [String] = [] |
| 8 | |
| 9 | override func setUp() { |
| 10 | continueAfterFailure = true |
| 11 | app = XCUIApplication() |
| 12 | app.launchArguments += ["-onboarded", "YES", "-locationMode", "city", "-nightMode", "off"] |
| 13 | app.launch() |
| 14 | } |
| 15 | |
| 16 | private func audit(_ screen: String, _ types: XCUIAccessibilityAuditType = .all) { |
| 17 | try? app.performAccessibilityAudit(for: types) { issue in |
| 18 | let label = issue.element?.label ?? "" |
| 19 | let id = issue.element?.identifier ?? "" |
| 20 | let line = "[\(screen)] \(issue.auditType): \(issue.compactDescription) — element: '\(label)' \(id)" |
| 21 | self.issues.append(line) |
| 22 | print("A11Y " + line) |
| 23 | return true |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | private func tab(_ name: String) { |
| 28 | app.tabBars.buttons[name].firstMatch.tap() |
| 29 | sleep(3) |
| 30 | } |
| 31 | |
| 32 | func testAuditAllScreens() { |
| 33 | sleep(3) |
| 34 | audit("Sky") |
| 35 | app.buttons["Settings"].tap(); sleep(1) |
| 36 | audit("Settings") |
| 37 | app.buttons["Done"].tap(); sleep(1) |
| 38 | tab("System"); audit("System") |
| 39 | tab("Sun"); audit("Sun") |
| 40 | tab("Launches"); audit("Launches") |
| 41 | tab("Library"); audit("Library") |
| 42 | app.staticTexts["Planets"].firstMatch.tap(); sleep(1) |
| 43 | app.staticTexts["Saturn"].firstMatch.tap(); sleep(2) |
| 44 | audit("Library page: Saturn") |
| 45 | print("ACCESSIBILITY-AUDIT-BEGIN") |
| 46 | issues.forEach { print($0) } |
| 47 | print("ACCESSIBILITY-AUDIT-END \(issues.count) issues") |
| 48 | } |
| 49 | } |
App/project.yml
+16
| @@ -10,6 +10,8 @@ targets: |
| 10 | 10 | Moonrise: |
| 11 | 11 | type: application |
| 12 | 12 | platform: iOS |
| 13 | scheme: |
| 14 | testTargets: [MoonriseUITests] |
| 13 | 15 | sources: [Sources, Shared] |
| 14 | 16 | dependencies: |
| 15 | 17 | - package: MoonriseKit |
| @@ -81,3 +83,17 @@ targets: |
| 81 | 83 | TARGETED_DEVICE_FAMILY: "1" |
| 82 | 84 | MARKETING_VERSION: "1.0" |
| 83 | 85 | CURRENT_PROJECT_VERSION: "1" |
| 86 | MoonriseUITests: |
| 87 | type: bundle.ui-testing |
| 88 | platform: iOS |
| 89 | sources: [UITests] |
| 90 | dependencies: |
| 91 | - target: Moonrise |
| 92 | settings: |
| 93 | base: |
| 94 | PRODUCT_BUNDLE_IDENTIFIER: sh.krz.moonrise.uitests |
| 95 | DEVELOPMENT_TEAM: ZCNAX3VL9D |
| 96 | CODE_SIGN_STYLE: Automatic |
| 97 | SWIFT_VERSION: "5.0" |
| 98 | TARGETED_DEVICE_FAMILY: "1" |
| 99 | GENERATE_INFOPLIST_FILE: YES |