krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

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 StatusTag(status: l.status, ink: ink, accent: accent) 64 StatusTag(status: l.status, ink: ink, accent: accent)
65 } 65 }
66 Text(l.rocket).font(.title2.weight(.medium)) 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 HStack { 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 Spacer() 70 Spacer()
71 Countdown(to: l.net).font(.footnote.weight(.medium).monospacedDigit()) 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 var body: some View { 66 var body: some View {
67 VStack(alignment: .leading, spacing: 2) { 67 VStack(alignment: .leading, spacing: 2) {
68 Text(page.title).font(.body) 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 .padding(.vertical, 2) 71 .padding(.vertical, 2)
72 } 72 }
@@ -111,6 +111,7 @@ struct LibraryPageView: View {
111 Text(f.label).font(.footnote).opacity(0.6) 111 Text(f.label).font(.footnote).opacity(0.6)
112 Text(f.value).font(.subheadline.monospacedDigit()) 112 Text(f.value).font(.subheadline.monospacedDigit())
113 } 113 }
114 .accessibilityElement(children: .combine)
114 Divider().overlay(ink.opacity(0.15)) 115 Divider().overlay(ink.opacity(0.15))
115 } 116 }
116 } 117 }
App/Sources/SkyScreen.swift +1
@@ -59,6 +59,7 @@ struct SkyScreen: View {
59 .sheet(isPresented: $showSettings) { 59 .sheet(isPresented: $showSettings) {
60 SettingsScreen { model.applyLocationSettings() } 60 SettingsScreen { model.applyLocationSettings() }
61 .preferredColorScheme(.dark) 61 .preferredColorScheme(.dark)
62 .tint(.blue)
62 } 63 }
63 .sheet(item: $libraryPage) { page in 64 .sheet(item: $libraryPage) { page in
64 NavigationStack { LibraryPageView(page: page) } 65 NavigationStack { LibraryPageView(page: page) }
App/Sources/SystemScreen.swift +6 −4
@@ -38,9 +38,11 @@ struct SystemScreen: View {
38 .font(.subheadline.weight(.medium).monospacedDigit()) 38 .font(.subheadline.weight(.medium).monospacedDigit())
39 .frame(width: 80, alignment: .leading) 39 .frame(width: 80, alignment: .leading)
40 Slider(value: $offsetDays, in: range).tint(ink) 40 Slider(value: $offsetDays, in: range).tint(ink)
41 Button("Today") { playing = false; offsetDays = 0 } 41 Button { playing = false; offsetDays = 0 } label: {
42 .font(.footnote.weight(.medium)) 42 Text("Today").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle())
43 .opacity(abs(offsetDays) < 1 ? 0.4 : 1) 43 }
44 .font(.footnote.weight(.medium))
45 .opacity(abs(offsetDays) < 1 ? 0.6 : 1)
44 } 46 }
45 .padding(.top, 12) 47 .padding(.top, 12)
46 48
@@ -58,7 +60,7 @@ struct SystemScreen: View {
58 ForEach(Spacecraft.all) { craft in 60 ForEach(Spacecraft.all) { craft in
59 GridRow { 61 GridRow {
60 if let page = Library.page(spacecraft: craft.name) { 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 } else { 64 } else {
63 Text(craft.name) 65 Text(craft.name)
64 } 66 }
App/Sources/TodaySheet.swift +8 −4
@@ -121,10 +121,12 @@ struct TodayBar: View {
121 scrubber 121 scrubber
122 Button(action: onExpand) { 122 Button(action: onExpand) {
123 HStack { 123 HStack {
124 Text(summary).font(.footnote).opacity(0.75).lineLimit(2) 124 Text(summary).font(.footnote).opacity(0.8)
125 Spacer() 125 Spacer()
126 Image(systemName: "chevron.up").font(.caption.weight(.medium)).opacity(0.6) 126 Image(systemName: "chevron.up").font(.caption.weight(.medium)).opacity(0.6)
127 } 127 }
128 .frame(minHeight: 44)
129 .contentShape(Rectangle())
128 } 130 }
129 .buttonStyle(.plain) 131 .buttonStyle(.plain)
130 .accessibilityHint("Shows tonight's details") 132 .accessibilityHint("Shows tonight's details")
@@ -157,9 +159,11 @@ struct TodayBar: View {
157 .font(.footnote.monospacedDigit()) 159 .font(.footnote.monospacedDigit())
158 .frame(width: 70, alignment: .leading) 160 .frame(width: 70, alignment: .leading)
159 Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink) 161 Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink)
160 Button("Now") { model.offsetHours = 0 } 162 Button { model.offsetHours = 0 } label: {
161 .font(.footnote.weight(.medium)) 163 Text("Now").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle())
162 .opacity(model.offsetHours == 0 ? 0.4 : 1) 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 @@
1import 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.
5final 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 Moonrise: 10 Moonrise:
11 type: application 11 type: application
12 platform: iOS 12 platform: iOS
13 scheme:
14 testTargets: [MoonriseUITests]
13 sources: [Sources, Shared] 15 sources: [Sources, Shared]
14 dependencies: 16 dependencies:
15 - package: MoonriseKit 17 - package: MoonriseKit
@@ -81,3 +83,17 @@ targets:
81 TARGETED_DEVICE_FAMILY: "1" 83 TARGETED_DEVICE_FAMILY: "1"
82 MARKETING_VERSION: "1.0" 84 MARKETING_VERSION: "1.0"
83 CURRENT_PROJECT_VERSION: "1" 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