Pre-Submission Quality Hardening (SP1) — Design¶
Datum: 2026-06-21 Status: Spec — zur Review Plattform-Ziel: iOS + iPad + tvOS (App-Store-Einreichung); macOS kommt SwiftUI-bedingt großteils gratis mit (bleibt Developer-ID-DMG, nicht im Store).
1. Kontext & Ziel¶
Physarum v2.2.0 ist feature-vollständig und test-grün (vitest 145/145, Swift-Engine ~43, native App-Host-Suite zuletzt macOS 282/0). Ziel der übergeordneten Runde: iOS + tvOS in den App Store einreichen (macOS bleibt der bestehende notarisierte DMG, kein Sandbox-Umbau — bewusste Weichenstellung 2026-06-21).
Die Bestandsaufnahme (3-Linsen-Audit) ergab: netto kaum noch offene Features (die großen Stränge — Preset-Slots/iCloud-Sync, Keep-only-pinned — sind in v2.1.0 released + geräte-abgenommen; der Rest ist Geräte-Sign-off [Johannes' HW-Aufgabe] oder bewusst geparkte Low-Value-Optimierung). Der eigentliche Hebel vor der Einreichung sind zwei echte, submission-relevante Quality-Lücken (kein bewusstes Tech-Debt):
- 🔴 Fehler-Handling: Nutzer-relevante Fehlerpfade (Mic-Permission-Denied, Capture-Save/Export-Fehler, Preset-/Audio-Import-Decode-Fehler) schlucken still.
AudioInputcachedpermissionDeniedohne Nutzer-Feedback; verstreute Ad-hoc-.alerts ohne gemeinsamen Mechanismus; CaptureSheet hat genau einen hardcodierten „Couldn't save"-Text. Schlechter erster Store-Eindruck. - 🟠 Accessibility:
Theme.Fontssind feste Größen (skalieren nicht mit Dynamic Type); nur eine Handvoll Controls haben VoiceOver-Labels (Canvas, Topbar, Preset-Cards, Matrix unlabeled).
Apple prüft beides (Guideline 1.5/Accessibility, und stumme Fehler sind ein UX-Mangel). Johannes' Wahl: voller Ausbau im Debut-Build.
2. Übergeordnete Zerlegung¶
Die Runde zerfällt in zwei Teilprojekte, je eigener Spec→Plan→Build:
- SP1 — Pre-Submission-Quality-Hardening (dieser Spec): Fehler-Handling-UX + Dynamic Type + VoiceOver + a11y-Tests. Formt den Build, der eingereicht wird.
- SP2 — App-Store-Submission-Readiness (Folge-Spec, §9): Privacy-Manifest,
app-store-Export-Config, Entitlements-/Bundle-ID-Verifikation, Screenshots, Metadaten-Entwurf, abhakbare ASC-Checkliste.
Reihenfolge: SP1 zuerst (bestimmt den eingereichten Build), dann SP2 (produziert das finale Archive + Assets). Johannes' ASC-Account-Seite (App-Records anlegen) läuft parallel.
3. Scope-Entscheidungen (ratifiziert)¶
| Frage | Entscheidung |
|---|---|
| Store-Plattformen | iOS + tvOS; macOS bleibt DMG (kein Sandbox-Umbau) |
| Hardening-Tiefe | Voll: Fehler-Handling + Dynamic Type durchgängig + VoiceOver auf allen Controls + a11y-Tests |
| a11y-Test-Form | Pure-Logik-XCTest (kein Image-Snapshot-Dependency); visuelle Prüfung = manueller Geräte-Pass |
4. Architektur¶
① Fehler-Handling — zentralisiert statt verstreut¶
Komponenten:
UserFacingError(Wert-Typ):title: String,message: String,recovery: RecoveryAction?.RecoveryAction=.openSettings(Deep-LinkUIApplication.openSettingsURLString/ macOS-Äquivalent) oder.none. Pure, host-testbar.ErrorPresenter(@Observable, im Environment): hältcurrent: UserFacingError?. API:report(_:),report(title:message:recovery:),dismiss(). Main-Thread-Contract (dispatchPrecondition(.onQueue(.main))an der Report-Schwelle wie bei den bestehenden Audio-Pfaden)..errorAlert(_ presenter:)— ein View-Modifier, gemountet einmal am App-Root: auf iOS/macOS anRootView(Sources/App/RootView.swift, äußererGeometryReader-Container); auf tvOS am tvOS-Root (Sources/TV/…). Rendert ein natives.alertmit Recovery-Button.
Report-Sites (die nutzer-relevanten Pfade):
| Site | Datei | heute | künftig |
|---|---|---|---|
| Mic-Permission-Denied | Sources/Audio/AudioInput.swift (permissionDenied) |
still gecached | presenter.report mit .openSettings beim Enable-Versuch trotz Denial |
| Capture-Save-Fehler | Sources/Capture/CaptureService.swift, CaptureSheet.swift:197 |
ein hardcodierter Text | über den Presenter, mit Kontext (Photos-Denied → .openSettings) |
| Capture-Export-Fehler | CaptureService.swift (.failure(error)) |
verworfen | gemeldet |
| Preset-Import-Decode | Sources/Library/LibraryView.swift (importError) |
lokaler State | auf den Presenter migriert (eine Quelle) |
| Audio-Config-Import | Sources/Editor/AudioReactiveSection.swift (importError) |
lokaler State | auf den Presenter migriert |
Bewusst NICHT angefasst: harmloses Housekeeping-try? (Temp-File-Cleanup in CaptureCleanup/VideoClipRecorder/ThumbnailRenderer, Order-Datei-Schreibungen) bleibt still — Surfacing dort wäre Lärm. Kriterium: ein try?/catch wird nur dann zum Report, wenn (a) der Nutzer die Aktion bewusst ausgelöst hat und (b) das Ergebnis sichtbar ausbleibt. Die bestehenden Confirmation-.alerts (Overwrite-Slot, Reset-all, Keep-pinned, Save-config) bleiben — sie sind Bestätigungen, keine Fehler.
② Dynamic Type — Theme von fix auf skalierend¶
Problem: Theme.Fonts (Sources/UI/PhysarumTheme.swift) sind Font.system(size: 10/11/13/15, …) — feste Punktgrößen, ignorieren die System-Textgröße. PhysarumTheme ist #if !os(tvOS) → betrifft iOS/iPad/macOS (tvOS hat eigene Fonts/Focus-UI, kein klassisches Dynamic Type).
Ansatz: semantische Font-Rollen auf relative Text-Styles abbilden, die mit Dynamic Type skalieren, den monospaced/SF-Mono-Look aber erhalten:
- eyebrow/label/value → monospaced, an einen relativen Style gebunden (.caption2/.caption/.footnote) statt fixer Größe (Font.system(.footnote, design: .monospaced) o. ä. mit Gewicht).
- title/body → .headline/.body.
- Hartkodierte Maße in Components.swift/Sliders, die der Schrift folgen müssen (Zeilenhöhen, Icon-neben-Text), via @ScaledMetric skalieren; reine Layout-Abstände (Theme.Space) bleiben fix.
Layout-Bruchstellen (bei großen Größen) auf adaptives Verhalten umstellen statt Clipping: Topbar, Editor-Panels (AudioReactiveSection, Sliders), Capture-Sheet → ViewThatFits / Umbruch / Scroll. AX-Cap nur dort, wo ein Control physisch nicht wachsen kann (z. B. kompakte Slider-Zeile) via .dynamicTypeSize(…upTo:) — sonst voll skalierend bis AX5.
③ VoiceOver — systematischer Durchlauf¶
Jeder interaktive Control: .accessibilityLabel + .accessibilityHint (+ .accessibilityValue für Slider/Toggles/Picker). Hints-Quelle = Sources/Model/ParamHelp.swift (existiert bereits, speist heute schon die In-App-Tooltips → kein neuer Text nötig).
- Canvas (
Sources/Player/MetalView.swift): wird ein einzelnes a11y-Element — Label „Physarum-Simulation", Value = aktueller Preset-Name → VoiceOver sagt an, was läuft (statt eines stummen Rechtecks). - Topbar-Buttons (Library/Editor/Fullscreen/Capture), Sidebar-Toggles, Preset-Cards (Label = Preset-Name + Pin/userOwned-Status), Matrix-Controls, Species-Selector bekommen sprechende Labels.
- Dekorative Cluster via
.accessibilityElement(children:)gruppieren/verstecken (aria-hidden-Äquivalent). - DRY: ein kleiner Helper (z. B.
View.physAccessible(label:hint:value:)oder eine Label-Provider-Funktion) hält die Pässe konsistent und macht die Logik testbar (siehe ④). - tvOS: kein Dynamic Type, aber VoiceOver + Focus-Engine — Focus-Elemente (Preset-Karussell, Overlay) bekommen Labels; der bestehende Focus-Mechanismus (Inc 12) bleibt unberührt.
④ a11y-Tests — pure-Logik-XCTest¶
Keine Image-Snapshots (kein neuer SPM-Dep, nicht flaky). Stattdessen: die a11y-relevante Logik in pure Funktionen ziehen und deterministisch testen:
- Label-/Hint-/Value-Provider (z. B. accessibilityLabel(for: control), accessibilityValue(for: paramID, value:)) → exakte Strings asserten, inkl. ParamHelp-Anbindung.
- Dynamic-Type-Skalierungs-Helfer (Font-Rollen-Mapping, etwaige @ScaledMetric-Berechnungen) → erwartete Skalierung pro Größenklasse asserten.
- UserFacingError-Konstruktion + Recovery-Mapping → Shape/Recovery-Action asserten.
Die visuelle Prüfung (VoiceOver-Sprachausgabe, Layout bei großem Text auf echtem Gerät) bleibt ehrlich ein manueller Geräte-Pass (Johannes' Sign-off, iOS/iPad + tvOS).
5. Komponenten-Isolation & Datenfluss¶
UserFacingError/RecoveryAction— reine Wert-Typen, keine Abhängigkeiten, voll testbar.ErrorPresenter— einzige Verantwortung: den aktuellen Fehler halten + dem Root signalisieren. Subsysteme kennen nur diereport-Schnittstelle, nicht die UI. Wird via Environment injiziert → Tests können einen Presenter ohne UI prüfen.- a11y-Provider — pure Funktionen, von den Views konsumiert; Views bleiben dünn.
- Datenfluss: Subsystem-Fehler →
presenter.report(main-hop) →ErrorPresenter.current→.errorAlertam Root → natives Alert → Recovery-Action (Settings-Deep-Link) oder Dismiss.
6. Was NICHT in Scope (YAGNI)¶
- Housekeeping-
try?(Cleanup/Order-Writes) bleibt still. - Kein macOS-Sandbox-Umbau (separate Weichenstellung; macOS bleibt DMG).
- Kein Image-Snapshot-Test-Framework.
- Keine neuen Features aus der „geparkt"-Liste (float16-Buffer, Bloom-Downsample, Tier C/D, CloudKit/Hue/ScreenCaptureKit).
- Dynamic Type nicht jenseits AX5; tvOS kein klassisches Dynamic Type.
- Kein Redesign — nur Härtung der bestehenden UI.
7. Test-Strategie & Gates¶
- Headless (CI-grün, Pflicht): bestehende App-Suite + Engine-Suite bleiben grün; neue pure-Logik-XCTests für ①/②/④; macOS+iOS+tvOS-Builds kompilieren;
tsc/biome/vitest unberührt (Web bleibt aus dem Scope). - Build-verifiziert: alle drei Apple-Targets bauen mit den Änderungen.
- Manueller Geräte-Pass (Johannes' Sign-off): VoiceOver durch die App (iOS/iPad + tvOS-Focus), großer Text (AX-Größen) ohne Clipping, Fehler-Alerts erscheinen (Mic-Denied → „Einstellungen öffnen", Photos-Denied, Import-Decode-Fehler).
8. Verifikation / DoD¶
- [ ]
UserFacingError+ErrorPresenter+.errorAlertam iOS/macOS- und tvOS-Root gemountet. - [ ] Alle 5 Report-Sites (§4①) gemeldet; Housekeeping-
try?unangetastet. - [ ] Mic-Denied bietet „Einstellungen öffnen".
- [ ]
Theme.Fontsskalieren mit Dynamic Type; Topbar/Editor/Capture-Sheet brechen nicht bei AX-Größen. - [ ] VoiceOver-Labels/Hints/Values auf allen interaktiven Controls + Canvas-a11y-Element.
- [ ] Pure-Logik-XCTests für Provider, Skalierung, Error-Shape — grün.
- [ ] macOS+iOS+tvOS-Build grün; bestehende Suites grün.
- [ ] Johannes' manueller Geräte-Pass abgenommen.
9. Folge-Teilprojekt SP2 — App-Store-Submission-Readiness (Outline)¶
Nach SP1, eigener Spec→Plan→Build. Damit nichts verloren geht, der Scope-Umriss:
- Privacy-Manifest
apple/PhysarumApp/PrivacyInfo.xcprivacyfür iOS + tvOS: deklariert genutzte APIs (AVAudioSession,PHPhotoLibraryadd-only,NSBonjourServices/Network). Plus App-Privacy-Nutrition-Label-Daten für ASC (Audio verlässt das Gerät nicht; Photos add-only; Local Network). app-store-Export-Config: separateExportOptions.plist-Variante (method: app-store,signingCertificate: Apple Distribution) — die bestehendedeveloper-id-Kette (macOS-DMG) bleibt unangetastet.- Entitlements-/Bundle-ID-Verifikation: iOS/tvOS-Sandbox ist automatisch; iCloud-KVS-Entitlement vorhanden. Bundle-ID
de.jkaindl.physarum(TeamU9X7M39R56). Single universal App-Record (iOS+tvOS) vs. getrennte Records — Empfehlung im SP2-Spec. - Encryption-Compliance:
ITSAppUsesNonExemptEncryption = false(nur SHA-256-Hashing). - Screenshots: Simulator-generiert (iPhone 6.7″, iPad 12.9″, tvOS 1080p) — Editor/Audio/Capture/Karussell.
- Metadaten-Entwurf: Name/Subtitle/Description/Keywords/Kategorie (Entertainment)/Age-Rating (4+); Pricing = Johannes' Produktentscheidung.
- Abhakbare ASC-Checkliste für die account-gebundenen Schritte (App-Records, Archive-Upload, „Submit for Review") — Johannes' interaktive Aufgabe.
10. Offene Annahmen / Risiken¶
- Annahme: Ich bereite alles bis zum „Submit"-Klick vor; die Apple-Account-gebundenen Schritte (ASC-Records, Upload, Submit) bleiben Johannes' (headless nicht ausführbar).
- Risiko (gering): Dynamic Type kann in dichten Panels (iPad-Editor) bei AX5 Layout-Druck erzeugen →
ViewThatFits/Scroll fangen das; im Geräte-Pass zu bestätigen. - Risiko (gering): tvOS-Fehlerquellen sind dünn (kein lokales Capture/Mic) — der Presenter wird trotzdem gemountet, Report-Sites dort minimal.
- Offen für den Plan: exakte Control-Inventur für den VoiceOver-Pass (Topbar/Sidebar/Cards/Matrix/Species) — wird in der Plan-Phase abschließend aus den View-Dateien gezogen.