Zum Inhalt

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. AudioInput cached permissionDenied ohne Nutzer-Feedback; verstreute Ad-hoc-.alerts ohne gemeinsamen Mechanismus; CaptureSheet hat genau einen hardcodierten „Couldn't save"-Text. Schlechter erster Store-Eindruck.
  • 🟠 Accessibility: Theme.Fonts sind 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-Link UIApplication.openSettingsURLString / macOS-Äquivalent) oder .none. Pure, host-testbar.
  • ErrorPresenter (@Observable, im Environment): hält current: 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 an RootView (Sources/App/RootView.swift, äußerer GeometryReader-Container); auf tvOS am tvOS-Root (Sources/TV/…). Rendert ein natives .alert mit 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 die report-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 → .errorAlert am 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 + .errorAlert am iOS/macOS- und tvOS-Root gemountet.
  • [ ] Alle 5 Report-Sites (§4①) gemeldet; Housekeeping-try? unangetastet.
  • [ ] Mic-Denied bietet „Einstellungen öffnen".
  • [ ] Theme.Fonts skalieren 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.xcprivacy für iOS + tvOS: deklariert genutzte APIs (AVAudioSession, PHPhotoLibrary add-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: separate ExportOptions.plist-Variante (method: app-store, signingCertificate: Apple Distribution) — die bestehende developer-id-Kette (macOS-DMG) bleibt unangetastet.
  • Entitlements-/Bundle-ID-Verifikation: iOS/tvOS-Sandbox ist automatisch; iCloud-KVS-Entitlement vorhanden. Bundle-ID de.jkaindl.physarum (Team U9X7M39R56). 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.