Files
certain/ios
GuidoandClaude Opus 5 bf437b3b69 feat: Zweite Stufe der iOS-App - Uebersicht, Budget, Buchungen, Vertraege
Die App kann jetzt lesen, wofuer man sie oeffnet. Vier neue Reiter:

- Uebersicht: Einnahmen/Ausgaben des Monats, Budget-Fortschritt je Gruppe,
  Kontostaende und die letzten Buchungen. Feste Auswahl statt des
  verschiebbaren Rasters der Weboberflaeche - auf Telefonbreite waere das
  Arbeit ohne Gewinn. Vier Endpunkte nebeneinander in einem Abruf, damit der
  Offline-Stand nicht aus vier verschiedenen Momenten stammt.
- Budget: Soll gegen Ist je Gruppe und Kategorie, blaetterbar ueber prev/next
  aus der Antwort. Rot nur bei echter Ueberschreitung - wer mehr zuruecklegt
  als geplant, hat nichts ueberzogen. Vertraege ohne Kategorie und fehlende
  Wechselkurse stehen als Hinweis darunter, statt die Summen still zu
  verfaelschen.
- Buchungen: Suche und Filter laufen im Server (Backend-Ergaenzung B), nicht
  im Telefon. Gesucht wird beim Absenden, nicht je Tastendruck.
- Vertraege: Monats- und Jahressumme, Rhythmus, naechste Faelligkeit.

Zwei Dinge, die das Backend bewusst dem Client ueberlaesst:

Ein Uebertrag kommt in zwei Gestalten - am Stueck (eine Buchung mit
to_account_id) oder als zwei verknuepfte Buchungen, wie die Bank sie liefert.
Beide Beine als zwei Zeilen zu zeigen hiesse, dasselbe Geld zweimal zu zeigen.
Buchungszeile.zeilen fasst sie zusammen, aber nur wenn beide in der Liste
stehen - sonst liesse ein Kontofilter Zeilen verschwinden, die sehr wohl zu
diesem Konto gehoeren. Die Richtung kommt aus der Verknuepfung, nicht aus dem
Vorzeichen: Manche Banken liefern beide Beine positiv.

Kategorien, Schlagwoerter und Konten liegen als Nachschlagewerk im AppZustand,
einmal geladen. Je Ansicht neu zu holen waere genau das N+1, das das Backend
sich selbst verbietet.

Texte in Deutsch und Englisch, zeilengleich. 44 Tests gruen, darunter die
Uebertrags-Paarung und jedes Modell gegen eine echte Serverantwort.

Nicht geprueft: der Weg gegen ein echtes Backend. Die App laesst nur https zu,
ein lokaler Server haette eine ATS-Ausnahme gebraucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-21 20:24:06 +02:00
..

Certain für iOS

Native App zum Certain-Backend.

Wer hier weiterarbeitet, liest zuerst UEBERGABE.md. Dort steht, was entschieden ist, was fertig ist, was als Nächstes dran ist und welche Eigenheiten das Backend hat. Dieses README erklärt nur das Projekt.

Bauen und starten

Einfach Certain.xcodeproj öffnen — es liegt mit im Repository.

Die Wahrheit ist aber project.yml: eine XcodeGen-Spezifikation, aus der das Projekt entsteht. Wer eine Swift-Datei anlegt, läuft danach einmal xcodegen generate, sonst kennt Xcode sie nicht. Ein neuer Ordner will ausserdem eine Zeile unter sources:.

brew install xcodegen          # einmalig
cd ios && xcodegen generate
xcodebuild -project Certain.xcodeproj -scheme Certain \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
xcodebuild -project Certain.xcodeproj -scheme Certain \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test

Festgelegt sind dort: Bündelkennung com.scarriffle.certain, Team PP34X97WS3, iPhone und iPad, Grundsprache Deutsch, iOS 26 als Mindestversion, Sprachfassung Swift 5. Wer mit einem anderen Apple-Konto baut, trägt seine Team-ID in project.yml ein.

Nur https. App Transport Security steht auf der Voreinstellung, ein Server unter http://… wird also abgewiesen — auch http://localhost beim Entwickeln. Das ist Absicht: über die Verbindung geht die Gerätemarke, ein Vollzugang. Wer eine Ausnahme braucht, trägt sie bewusst in project.yml ein.

Was wohin gehört

Ordner Inhalt
App/ Einstiegspunkt, Navigation, globaler Zustand
Netzwerk/ Client, Endpunkte, Fehler, Offline-Ablage
Modelle/ Codable-Typen, eins zu eins zu den Backend-Schemas
Bausteine/ Wiederverwendetes: Geld, Farben, Symbole, Zustandsansichten
Anmeldung/ Server-Adresse, Anmeldung, zweiter Faktor, Passwortwechsel
Konten/ Kontenliste (die erste Fachansicht)
Texte/ Übersetzungen
Tests/ Reine Logiktests, ohne Netz

Wie die Anmeldung funktioniert

Das Backend kennt für die Weboberfläche nur ein Sitzungs-Cookie. Für die App wurde eine Gerätemarke der Reichweite app ergänzt (backend/app/models.py, TokenScope). Der Ablauf:

  1. Nutzer gibt die Server-Adresse ein. Die App prüft mit GET /api/version, ob dort überhaupt ein Certain läuft.
  2. Nutzer meldet sich mit Benutzername und Passwort an — über das normale Cookie, inklusive zweitem Faktor und Pflicht-Passwortwechsel, falls fällig.
  3. Noch in derselben Sitzung legt die App über POST /api/inbox/tokens mit scope: "app" eine Marke an und legt deren Klartext im Schlüsselbund ab.
  4. Danach läuft alles über den Header X-Certain-Token. Das Cookie wird verworfen.

Damit gibt es kein Passwort auf dem Gerät und keinen Ablauf nach 30 Tagen. Die Marke lässt sich am Rechner unter Einstellungen → Gerätemarken einzeln widerrufen; danach sperrt sie sofort.

Eine App-Marke kann keine weitere Marke ausstellen — das geht nur mit einer echten Anmeldung. Sonst hätte ein Dieb sich Ersatzschlüssel ausgestellt und das Widerrufen wäre wirkungslos.

Später: Erweiterungen

Teilen-Menü und Kurzbefehle kommen in Stufe 6. Beide brauchen Zugriff auf dieselbe Marke wie die App. Dafür ist im Projekt nötig:

  • eine App-Gruppe (group.<deine-bundle-id>) für die Server-Adresse,
  • eine geteilte Schlüsselbund-Gruppe für die Marke selbst.

Netzwerk/Ablage.swift liest beides bereits aus Konstanten, die du an einer Stelle setzt (Ablage.gruppe). Solange keine Erweiterung existiert, funktioniert alles auch ohne App-Gruppe.

Was die App bewusst nicht kann

  • Bankkonten verbinden. Die Bank leitet auf eine feste Web-Adresse zurück, die bei Enable Banking registriert ist. Abrufen und Zuordnen geht nativ, das Herstellen der Verbindung bleibt im Browser.
  • Postfach einrichten und Sicherung/Wiederherstellung. Gehört an den Rechner.
  • Benutzerverwaltung. Administration für Mehrbenutzerbetrieb.

Was geprüft ist — und was nicht

Geprüft: Jedes Feld der Modelle wurde gegen echte Antworten eines laufenden Backends verglichen (/api/version, /api/auth/me, /api/accounts, /api/settings, /api/inbox/tokens). Alle Namen stimmen überein. Dabei kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt (2026-10-20T14:18:15.780705) — dafür hat Zeitpunkt einen dritten Leser, und GrundlagenTests deckt den Fall ab.

Ebenfalls geprüft: Dass es übersetzt. Ein einziger Fehler in 2600 blind geschriebenen Zeilen ([:] statt [] in CertainClient.schickeDatei), danach keine Warnung im eigenen Code, 28 Tests grün, Start im Simulator bis zur Serverabfrage.

Nicht geprüft: Der Weg gegen ein echtes Backend — anmelden, zweiter Faktor, Marke anlegen, Konten laden.

Wo es beim ersten Bauen am ehesten klemmte

Nichts davon trat ein; die Liste bleibt als Landkarte der heiklen Stellen.

  1. @testable import Certain in Tests/GrundlagenTests.swift — muss auf den Namen deines App-Ziels zeigen. Heisst es anders, anpassen.
  2. Lader ist eine generische @Observable-Klasse. Das geht, ist aber die ungewöhnlichste Konstruktion hier. Falls der Übersetzer murrt: Die Klasse wird bisher nur an einer Stelle benutzt (KontenListe).
  3. Nebenläufigkeit. AppZustand ist @MainActor, CertainClient ein Actor. Unter strikter Nebenläufigkeitsprüfung kann es an den Übergängen Warnungen setzen.
  4. Tab und .tabViewStyle(.sidebarAdaptable) in RootAnsicht setzen iOS 18 voraus. Bei iOS 26 als Mindestversion kein Problem.
  5. Die Tests nutzen Swift Testing (import Testing, @Test, #expect), nicht XCTest. Xcode 16 und neuer kann beides im selben Ziel.

Schick mir die Fehler, dann räumen wir sie weg.

Stand

Stufe 1: Gerüst, Anmeldung samt zweitem Faktor und Passwortwechsel, Kontenliste. 21 Swift-Dateien, rund 2600 Zeilen. Siehe den Plan im Repository für die weiteren Stufen.

Noch nicht in der App, obwohl in Stufe 1 erreichbar: Einen zweiten Faktor einrichten geht nur im Browser. Wer ein Konto hat, für das die Richtlinie MFA verlangt, sieht deshalb einen Hinweis mit Link statt eines Einrichtungs- Assistenten. Das kommt mit den Einstellungen in Stufe 5.