# Plan — cards-native (SwiftUI Universal) **Stand: 2026-05-13 — Phasen β-0 bis β-7 abgeschlossen.** Feature-komplett für TestFlight. Alle 7 Card-Types + Marketplace + Keyboard/Daily-Reminder/Widget + Siri-Shortcut + Share-Extension + App-Icon-Platzhalter + Release-Checklist. 35 Unit-Tests + 1 UI-Test grün, alle drei Targets (Haupt-App + Widget + Share) bauen. Pflicht-Check für β-2: Endurance-Test auf realem Gerät (200+ Karten mit Flugmodus zwischendurch) steht aus — Aufgabe für Till. > **SOT:** `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`. > Dieses File ist die App-lokale Status-Spur, das Greenfield-Doc > hat die ganze Architektur-Begründung. ## Aktueller Stand ✅ **β-0 — Setup (2026-05-12, Tag `v0.1.0`)** - Repo-Skelett unter `git.mana.how/till/cards-native` - `project.yml` mit Bundle-ID `ev.mana.cards`, ManaSwiftCore via `path: ../mana-swift-core` - `AppConfig` als `ManaAppConfig`-Provider: - Auth: `https://auth.mana.how` - API: `https://cardecky-api.mana.how` - Keychain-Service: `ev.mana.cards` - `CardsTheme.swift` mit forest-Werten (lokal nachgebaut aus `mana/packages/themes/src/variants/forest.css`) - `LoginView` (Email/PW gegen mana-auth) - 3 Unit-Tests (AppConfig) ✅ **β-7 — App-Store-Vorbereitung (2026-05-13, Tag `v0.8.0`)** - App-Icon-Platzhalter: `scripts/make-appicon.swift` generiert 1024×1024 PNG aus CoreGraphics (forest-green + "C"-Letter). Asset-Catalog auf Single-Size-Pattern umgestellt. **Vor App-Store-Submit durch Designer- Icon ersetzen** (siehe `docs/RELEASE_CHECKLIST.md`). - `StudyCardsIntent` + `CardsAppShortcuts` (App Intents Framework): Siri-Shortcut "Karten lernen mit Cards" / "Mit Cards lernen", öffnet die App, App-Shortcut-Provider macht ihn ohne Konfiguration sichtbar. - `CardsShareExtension`-Target (app-extension): empfängt Text/URL aus Safari/Mail-Share-Sheets, SwiftUI-Mini-Editor, persistiert `PendingShare` in App-Group. Haupt-App zeigt Banner in DeckListView, Tap → `PendingShareConsumeView` mit Deck-Picker + Front/Back-Felder, Submit → `POST /cards`, danach `PendingShareStore.remove`. - `PendingShare` + `PendingShareStore` shared in beiden Targets. - `NSPhotoLibraryUsageDescription` + `NSUserActivityTypes` in Info.plist ergänzt für Image-Occlusion-Picker und Universal-Links. - `docs/RELEASE_CHECKLIST.md` — externe Schritte: Apple-Developer- Portal-Konfiguration, AASA-Endpoint, TestFlight-Test-Plan, App-Store- Connect-Felder, Compliance-Verifikation. - UI-Test robuster gegen Keychain-State (akzeptiert sowohl Login als auch Decks/Entdecken als gestartete App). ✅ **β-6 — Native-Polish (2026-05-13, Tag `v0.7.0`)** - Keyboard-Shortcuts in `StudySessionView`: Space = flip, 1/2/3/4 = again/hard/good/easy (über hidden Buttons mit `.keyboardShortcut(.space/KeyEquivalent)`, iPad-Magic-Keyboard + macOS-tauglich) - `NotificationManager` @Observable: Permission-Request, Authorization-State, täglicher `UNCalendarNotificationTrigger` zur konfigurierten Uhrzeit (UserDefaults-Persistierung) - `SettingsView` (in AccountView verlinkt): Toggle + DatePicker für Reminder, "Über"-Section mit Server-URLs - `WidgetSnapshot` Codable mit `topDecks` (Top-3 nach dueCount) und `totalDueCount` - `WidgetSnapshotStore` schreibt in App-Group-Container `group.ev.mana.cards` - `DeckListStore.refresh` ruft `updateWidgetSnapshot()` und `WidgetCenter.shared.reloadAllTimelines()` nach jedem Pull - `CardsWidgetExtension`-Target (eigenes app-extension-Bundle): `CardsWidgetBundle` + `CardsDueWidget` mit `StaticConfiguration`, Support für systemSmall, systemMedium, accessoryCircular, accessoryInline, accessoryRectangular - `DueProvider` als `TimelineProvider`: liest Snapshot, plant Refresh alle 30 min (plus instant-Refresh via Haupt-App) - `DueWidgetView` mit Family-Switch, alle 5 Family-Layouts - `com.apple.security.application-groups: group.ev.mana.cards` im Haupt- und Widget-Entitlement - `WidgetSnapshot.swift` in beiden Targets via XcodeGen-source-array (single-source-of-truth) **Deferred auf β-7:** Siri-Shortcuts (App Intents), Share-Extension für Save-as-Card. Niedrige Priorität — Keyboard + Notifications + Widget decken 90% des Native-Polish ab. ✅ **β-5 — Marketplace (2026-05-13, Tag `v0.6.0`)** - `PublicDeckEntry`, `PublicDeck`, `PublicDeckVersion`, `PublicDeckOwner`, `PublicDeckDetail`, `ExploreResponse`, `BrowseResponse`, `SubscribeResponse` Codable-DTOs mit snake_case - `MarketplaceSort` Enum (recent/popular/trending) mit deutschen Labels - `CardsAPI`: explore(), browseMarketplace(query:sort:language:), publicDeck(slug:), subscribe(slug:), unsubscribe(slug:) - `MarketplaceStore` @Observable mit Explore-State + Browse-State - `ExploreView` mit Featured + Trending Carousels, Browse-Link - `BrowseView` mit Searchable + Sort-Picker + Liste - `PublicDeckView` mit Header + Version + Owner + Subscribe-Button (Auto-Fork serverseitig, danach NavigationLink zum eigenen Deck) - `PublicDeckCard` + `BrowseRow` Komponenten mit forest-Theme - `RootView` → TabBar (Decks / Entdecken / Account) statt Single-View - Universal-Link-Handler in `RootView` (onOpenURL + onContinueUserActivity): `https://cardecky.mana.how/d/` und `cards://d/` → Explore-Tab öffnet `PublicDeckView` - `associated-domains: applinks:cardecky.mana.how` im entitlement - 5 neue Marketplace-Decoding-Tests (35 Total grün) **Wichtig:** Universal-Links funktionieren erst, wenn AASA-Endpoint unter `cardecky.mana.how/.well-known/apple-app-site-association` ausgeliefert wird — heute 404. Web-seitige Aufgabe. ✅ **β-4 — Media + Advanced Card-Types (2026-05-13, Tag `v0.5.0`)** - `MediaUploadResponse` DTO + `MediaKind`-Enum - `MaskRegion` Codable mit 0..1-Coordinates, `MaskRegions.parse/encode`- Helpers (1:1-Port aus `cards-domain/image-occlusion.ts` — Sortierung nach ID lexikographisch) - `CardFieldsBuilder.imageOcclusion`, `.audioFront` mit korrekter `mask_regions`-Serialisierung als stringified JSON-Array - `CardsAPI.uploadMedia(data, filename, mimeType)` mit Multipart (25 MiB max), `.fetchMedia(id)` für streamed bytes - `MediaCache` actor mit LRU 200 MB (sortiert nach `contentModificationDate`) - `mediaCache`-Environment-Key, im App-Entrypoint instantiiert - `RemoteImage` View — authentifiziertes Image-Loading mit ProgressView + Failure-State - `AudioPlayerButton` — AVAudioPlayer-Wrapper mit Play/Pause-Toggle, AVAudioSession-Setup für iOS - `CardRenderer.imageOcclusionView`: AsyncImage + opake Maske über aktiver Region (Frontside), Label-Reveal auf Backside - `CardRenderer.audioFrontView`: AudioPlayerButton + back-Text auf Flip - `MaskEditorView`: Touch-Drag-to-Create-Rectangle, Liste mit Label-Edit + Delete, 0..1-Normalisierung beim Commit - `CardEditorView` erweitert: PhotosPicker für Image, fileImporter für Audio, Magic-Byte-MIME-Detection (JPEG/PNG/GIF/WebP) - 6 neue Tests für MaskRegions-Parse/Encode + Field-Builder (30 Total) ✅ **β-3 — Editor (2026-05-13, Tag `v0.4.0`)** - `DeckCreateBody`, `DeckUpdateBody`, `CardCreateBody`, `CardUpdateBody` Encodable-Structs (snake_case via `CodingKeys`, nil-Felder werden weggelassen) - `CardFieldsBuilder` mit Type-spezifischen Pflicht-Feld-Konstruktoren - `CardsAPI`: createDeck/updateDeck/deleteDeck + createCard/updateCard/deleteCard - `DeckEditorView` für Create + Edit in einer View (mode-switch), Color-Picker mit 8-Preset-Palette aus forest-Theme, Category-Picker (11 Kategorien mit deutschen Labels), Visibility-Segmented-Control - `CardEditorView` mit Type-Picker (basic, basic-reverse, cloze, typing, multiple-choice) und dynamischen Feldern je Typ. Cloze-View zeigt Live-Cluster-Count und Hint-Syntax-Hinweis. image-occlusion und audio-front zeigen β-4-Placeholder - `DeckDetailView` mit 4 Action-Buttons (Lernen, Karte hinzufügen, Bearbeiten, Löschen), Confirmation-Dialog für Delete - DeckListView: "+"-Button im Toolbar (Leading), Sheet für Create - 7 zusätzliche Encoding-Tests (24 Unit-Tests total) ✅ **β-2 — Study-Loop (2026-05-13, Tag `v0.3.0`)** - `Card`, `Review`, `DueReview` Codable-DTOs, `CardType`-Enum (alle 7 Typen) - `Rating`-Enum: `again | hard | good | easy` mit deutschen Labels - `Cloze`-Helpers (extractClusterIds, subIndexCount, clusterId, renderPrompt, renderAnswer, hint) — 1:1-Port aus `cards/packages/cards-domain/src/cloze.ts` - `CardsAPI.dueReviews(deckId:)`, `CardsAPI.gradeReview(...)` mit ISO8601-Encoder - `PendingGrade` SwiftData-Model + `GradeQueue` für Offline-Submit (FIFO-Drain, originaler reviewedAt-Timestamp bleibt erhalten) - `StudySession` als @Observable State-Machine (loading/studying/finished/failed) - `CardRenderer`: basic, basic-reverse (sub-index-abhängig), cloze client-rendered. image-occlusion/audio-front/typing/multiple-choice zeigen Placeholder (β-3/β-4) - `RatingBar` mit Haptic-Feedback (medium für again/hard/good, heavy für easy, soft beim Flip) - `StudySessionView` vollbild aus DeckListView per NavigationLink - 9 zusätzliche Tests (Cloze 8x, Review/DueReview-Decoding 3x) ✅ **β-1 — Decks lesen (2026-05-13, Tag `v0.2.0`)** - `Deck`-Codable-DTO mit snake_case-CodingKeys, plus `DeckCategory`, `DeckVisibility`, `FsrsSettings` - ISO8601-Date-Decoder mit Fractional-Seconds-Toleranz - `CardsAPI.listDecks()`, `cardCount(deckId:)`, `dueCount(deckId:)` - `CachedDeck` als SwiftData-Model mit `lastFetchedAt` (Offline-Read) - `DeckListStore` orchestriert API + Cache, paralleles Counts-Fetching via TaskGroup - `DeckListView` mit Pull-to-Refresh, Card/Due-Counts, deck.color-Streifen, Inbox-Banner für Marketplace-Forks - `AccountView` mit Sign-out-Button - iOS-Simulator-Build + Tests grün (6 Unit-Tests, 1 UI-Test) ## Phasen (Detail in Greenfield-Plan) | Phase | Status | Inhalt | |---|---|---| | β-0 | ✅ 2026-05-12 | Setup, Login, API-Probe | | β-1 | ✅ 2026-05-13 | Decks lesen, SwiftData-Cache, Pull-to-Refresh | | β-2 | ✅ 2026-05-13 | Study-Loop, Offline-Grade-Queue (Endurance-Test offen) | | β-3 | ✅ 2026-05-13 | Editor: Deck-CRUD + Card-Create (5 Types); Anki-Import auf β-3-ext verschoben | | β-4 | ✅ 2026-05-13 | Media-Upload, image-occlusion (Touch-Mask-Editor), audio-front (AVAudioPlayer) | | β-5 | ✅ 2026-05-13 | Marketplace (Explore/Browse/Subscribe) + TabBar + Universal-Link-Handler (AASA server-side pending) | | β-6 | ✅ 2026-05-13 | Keyboard-Shortcuts + Daily-Reminders + WidgetKit (Siri/Share deferred auf β-7) | | β-7 | ✅ 2026-05-13 | App-Icon-Platzhalter + Siri-Shortcut + Share-Extension + Release-Checklist (externe Apple-Schritte siehe docs/RELEASE_CHECKLIST.md) | ## Nächste Schritte: TestFlight + App-Store Alle remaining steps sind **externe Aktionen** außerhalb des Repos — Apple-Developer-Portal, App-Store-Connect, Xcode-Archive, das Cards-Web-Repo (AASA). Strukturierte Liste in [`docs/RELEASE_CHECKLIST.md`](docs/RELEASE_CHECKLIST.md): 1. Apple-Developer-Konfiguration (Team-ID, App-IDs, App-Group, Profiles) 2. App-Icon-Platzhalter durch Designer-Icon ersetzen 3. AASA-Endpoint auf `cardecky.mana.how` (Cards-Web-Repo) 4. Xcode-Archive + TestFlight-Upload 5. Endurance- und Cross-Device-Tests im TestFlight-Beta 6. App-Store-Connect-Listing (Description, Screenshots, Privacy) 7. Submission ## Notizen aus β-4 - **PencilKit für Mask-Editor explizit nicht genutzt.** Web macht Image-Occlusion-Masks per Touch-Drag-Rechteck (kein Freihand). Server- Schema (`MaskRegion = {id, x, y, w, h, label}`) erlaubt nur Rechtecke, PencilKit-Strokes wären dafür übersteigert. Wenn später Polygon-Masks oder Freihand-Skizzen dazu kommen, kann PencilKit nachgereicht werden. - **Apple-Pencil-Support** trotzdem grundsätzlich da: SwiftUI's `DragGesture` reagiert auf Pencil-Eingaben genauso wie auf Finger. ## Verschoben auf β-3-Extension oder später - **Anki-Import** (`.apkg`-Parser): Web parsed client-side und ruft `POST /cards` pro Karte. Native bräuchte eigenen Swift-Parser für Anki-Pakete (Plist/sqlite/.apkg) — eigener Brocken, nicht blockierend für andere Phasen. - **Card-Edit** (PATCH /cards/:id): Card-Create reicht für Web-Parität in v1, Edit kann später nachgereicht werden. - **Distractor-Vorschau** für Multiple-Choice-Editor: Server liefert Distractors zur Lernzeit (`/decks/:deckId/distractors`), Editor zeigt sie nicht — Web macht das auch nicht. ## Pflicht-Tests für β-2 (vor β-3-Start) - [ ] Endurance-Test auf realem Gerät: 200+ Karten lernen, Flugmodus zwischendurch — alle Grades landen am Server nach Reconnect. - [ ] Cross-Check mit Web: Karte gegrade in Native → Web zeigt identischen Review-State nach Reload. ## Cross-Refs - `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` — Greenfield-Plan SOT - `../mana/docs/MANA_SWIFT.md` — Plattform-SOT - `../cards/CLAUDE.md` — Cards-Repo - `../cards/STATUS.md` — Web-Phasenstand (Referenz) - `../mana-swift-core/CLAUDE.md` — ManaCore-Konventionen - `CLAUDE.md` — Repo-Konventionen