iOS: Pakete, Start und die Relay-Verbindung
Wie die iPhone-App in Swift-Pakete aufgeteilt ist, warum der Start auf den Schlüsselbund wartet, und wie ihr Network.framework-Socket sich registriert, Zeitlimits einhält, sich erholt und Live-Events filtert.
Gegen den Quellcode geprüft am 17. September 2026
Auf dieser Seite
Drei Swift-Pakete und ein schlankes App-Target
Das Xcode-Projekt hat zwei Targets: die PlanToCode-App und ein VibeUITests-Bundle, das in ihr gehostet läuft. Das App-Target enthält drei Swift-Dateien: den SwiftUI-Einstiegspunkt, den App-Delegate und eine Delegate-Erweiterung für das Timeline-Interesse. Es bindet Core und VibeUI direkt ein, und sein Delegate ruft Core für Push, den Hintergrund und die Wiederherstellung auf. Alles andere liegt in drei lokalen Swift-Paketen, gebaut mit Swift 6.3 für iOS 18.
| Target oder Paket | Was es enthält | Hängt ab von |
|---|---|---|
| App-Target | Drei Swift-Dateien: der SwiftUI-Einstiegspunkt, der App-Delegate für Start, Push und Hintergrund sowie seine Erweiterung für das Timeline-Interesse. | Core und VibeUI. |
| VibeUI | Jeder Bildschirm, die UIKit-Chat-Collection, die Code- und Dokumentbetrachter, der Review Mode und die gebündelte mermaid.min.js. | Core, VibeUIFormatting, Runestone mit 37 Tree-sitter-Grammatiken, swift-syntax, swift-cmark, MarkdownUI und SwiftMath. |
| Core | Ein Target aus Sources/Core und Sources/Security: der Relay-Client, Anmeldung und Tokens, der RPC-Command-Router, Datendienste, StoreKit, Push, Sprache und die Schlüsselbund-Wrapper. | KeychainAccess. |
| VibeUIFormatting | Markdown-Texthelfer: der Pfadklassifizierer, der Linkifier für Inline-Referenzen und der Normalisierer für nummerierte Listen. | Nichts. |
| VibeUITests | Das Test-Bundle, gehostet in der App. | Core, VibeUI, VibeUIFormatting, Runestone und MarkdownUI. |
Core kann VibeUI nicht importieren, deshalb bauen und testen Relay-Client, Anmeldung und Datendienste ohne einen einzigen Bildschirm.
| Pfad vom Repository-Stammverzeichnis | Beginnen Sie hier für |
|---|---|
| mobile/ios/App/ | Start, Push-Registrierung und die Übergänge zwischen Hintergrund und Vordergrund. |
| mobile/ios/Core/Sources/Core/Connectivity/ | Der Relay-Socket, die Registrierung, Wiederherstellungsepochen und das Timeline-Interesse-Lease. |
| mobile/ios/Core/Sources/Core/Data/Manager/Ingress/ | Annahme von Relay-Events vor dem Main Actor. |
| mobile/ios/VibeUI/Sources/VibeUI/Features/Workspace/Chat/ | Chat-Hosts, die Collection-View, die Outbox und Chat-Vorschauen. |
| mobile/ios/VibeUI/Sources/VibeUI/Features/Workspace/ReviewMode/ | Aufnahme, Transkription und Videokomposition im Review Mode. |
Der Start wartet auf den Schlüsselbund
- Geräte-IDAfterFirstUnlockThisDeviceOnly
Die Identität des Telefons für die Geräteliste und das Relay. Sie wird nie mit iCloud synchronisiert. Die Plattform hält diese Klasse lesbar, während das Telefon gesperrt ist, aber nicht vor dem ersten Entsperren nach einem Neustart.
- App-JWT und Resume-TokensWhenUnlockedThisDeviceOnly
Der Standard für die Schlüsselbundeinträge der App. Die Plattform macht diese Klasse nur lesbar, solange das Telefon entsperrt ist.
- StartprotectedDataDidBecomeAvailable
Der App-Delegate und AppView lesen die Geräte-ID abseits des Main Threads. Solange sie nicht lesbar ist, startet Core nicht, und AppView behält seinen Ladebildschirm. Beide versuchen es erneut, wenn geschützte Daten verfügbar werden oder die App in den Vordergrund zurückkehrt.
- AnmeldeprüfungcheckStoredToken
Ist das App-JWT nicht lesbar, weil das Telefon gesperrt ist, bleibt die gespeicherte Anmeldung erhalten. Die Prüfung läuft erneut, wenn geschützte Daten verfügbar werden oder die App aktiv wird.
Die Geräte-ID ist die Identität des Telefons für die Geräteliste und das Relay, deshalb startet nichts, was mit dem Netzwerk spricht, ohne sie. Sie ist ein Schlüsselbundeintrag mit der Zugänglichkeit AfterFirstUnlockThisDeviceOnly, der nie mit iCloud synchronisiert wird. Das App-JWT, sein Ablaufzeitpunkt und die Relay-Resume-Tokens verwenden den Standard für die Einträge der App, WhenUnlockedThisDeviceOnly.
Der App-Delegate liest die Geräte-ID in einer Hintergrundaufgabe, bevor er Core initialisiert, und AppView löst die Identität ebenfalls abseits des Main Threads auf. Bei einem Start im Hintergrund vor dem ersten Entsperren bleibt AppView auf dem Ladebildschirm und versucht es erneut, sobald das System protectedDataDidBecomeAvailable meldet. Während das Telefon gesperrt ist, hält die Plattform die Geräte-ID lesbar, das App-JWT aber nicht, deshalb behält ein Start in diesem Zustand die gespeicherte Anmeldung und prüft sie erneut, sobald geschützte Daten verfügbar sind oder die App aktiv wird. Ein Eintrag, der sich gar nicht lesen lässt, bietet Reset Device Identity an.
Die Anmeldung braucht keine Callback-URL. ASWebAuthenticationSession öffnet /auth/auth0/initiate-login mit callbackURLScheme auf nil, und die App fragt /auth0/poll-status alle 2 Sekunden ab, bis zu 60 Mal, bevor sie den Code mit ihrem PKCE-Verifier eintauscht und die Anmeldung abschließt. Es läuft immer nur eine Erneuerung des App-JWT. Ihr Timer feuert 300 Sekunden vor dem Ablauf, frühestens aber in 5 Sekunden, und ein 401 oder 403 vom Erneuerungsendpunkt beendet die Sitzung.
Die gewählte Region wird in UserDefaults gespeichert. Ihre Basis-URL, https://api-us.plantocode.com oder https://api-eu.plantocode.com, bestimmt sowohl den REST-Client als auch den /ws/device-link-Socket. iOS speichert die gewählte Region, bevor es den Navigationszustand veröffentlicht, und bindet jeden Registrierungsversuch an den Ursprung, den er verwendet hat.
Ein WebSocket auf Network.framework
Der Relay-Socket ist eine NWConnection mit NWProtocolWebSocket: Version 13, automatische Antworten auf Pings und 32 MiB maximale Nachrichtengröße. Die Parameter erlauben teure und eingeschränkte Pfade und verwenden den Multipath-Dienst handover, sodass die Verbindung zwischen WLAN und Mobilfunk wechseln kann. Nur wss-URLs werden akzeptiert. Die Upgrade-Anfrage trägt das App-JWT als Bearer-Token, X-Device-ID und X-Token-Binding mit der Geräte-ID, X-Client-Type: mobile und X-Target-Desktop-Device-ID für den gewählten Desktop.
Jeder Socket erhält eine Transportgeneration. Das Zustandsobjekt, das einen Socket installiert, führt unter derselben Sperre auch Sendungen aus, die an eine Generation gebunden sind, deshalb kann eine für einen früheren Socket vorbereitete Sendung nie dessen Nachfolger erreichen. Zustands-Callbacks einer Verbindung, die nicht mehr aktuell ist, werden ignoriert.
Nach dem Upgrade sendet der Client register mit der Geräte-ID, dem Gerätenamen, relayProtocolVersion 1.6, dem Zieldesktop sowie der Relay-Session-ID und dem Resume-Token, wenn er sie hat. Der Handshake läuft nach 10 Sekunden ab, und eine Sendung, die aus einem vorübergehenden Grund scheitert, wird nach min(0.25 × attempt, 1.5) Sekunden wiederholt. Nach der Registrierung sendet der Client alle 5 Sekunden eine eigene heartbeat-Nachricht, und ein Watchdog lässt die Verbindung nach 15 Sekunden ohne eingehenden Verkehr scheitern.
Resume-Tokens liegen pro Paar aus Telefon und Desktop im Schlüsselbund, unter dem Schlüssel <device ID>::<desktop ID>. Ein serieller Speicher gibt jedem Relay-Client eine Besitzgeneration für seinen Schlüssel und weist Schreibvorgänge eines Clients ab, dem er nicht mehr gehört, sodass ein veralteter Client kein neueres Token überschreiben kann. Ein invalidResume-Fehler lässt den Client sich nach 0,2 Sekunden ohne Token neu registrieren. Drei ungültige Resumes innerhalb von rollierenden 30 Sekunden beenden die Schleife mit invalidResumeLoop.
Anfragen, Zeitlimits und Trace-Kontext
RpcMethod listet 65 Methoden, und 22 davon verlangen einen Idempotenzschlüssel. Der Client weist eine unbekannte Methode oder eine Mutation ohne Schlüssel ab, bevor etwas gesendet wird, und der ausgehende Encoder wirft einen Fehler, sobald er irgendwo in einer Payload einen snake_case-Schlüssel findet. Ausstehende Aufrufe sind nach Anfrage-ID zusammen mit einem Besitz-Token geordnet. Eine zweite laufende Anfrage mit derselben ID wird abgewiesen, und das Aufräumen eines alten Aufrufs kann keinen neueren entfernen.
| Methoden | Standard-Zeitlimit |
|---|---|
| files.readContent | 8 Sekunden. |
| Dateibäume, Run-Liste und -Abruf, Nutzungslimits, Auth-Profile, Sprachvokabular und -kontext, Verlaufszustand | 90 Sekunden. |
| Session- und Projektlisten und -Aktualisierungen, Abgleich, bestehende Sessions, Abbrechen und Löschen, Diffs, Befehlsausgabe, Webseiten als Markdown, Binär- und Medienlesevorgänge, Anhang-Uploads | 60 Sekunden. |
| Alles andere | 30 Sekunden. |
| Chat-Richtlinie: erste Timeline, ältere Seite, Outbox-Bestätigung | 115, 60 und 20 Sekunden. |
Jede Anfrage trägt einen W3C-traceparent, und eine Antwort wird abgewiesen, wenn ihre Trace-ID nicht zu der der Anfrage passt. Ein Zeitlimit lässt diesen Aufruf scheitern und verbindet die Socket-Generation neu, die ihn gesendet hat. Wurde dieser Socket bereits ersetzt, berührt das Zeitlimit nichts anderes.
Wiederherstellungsepochen, Netzwerkwechsel und der Hintergrund
- Epoche der WiederherstellungdesktopReconnectGraceSeconds = 90
Eine pro Desktop. Sie endet zur reconnectDeadline, die das Relay geschickt hat, oder ohne sie 90 Sekunden, nachdem das Telefon den Verlust bemerkt hat. Stille, ein RPC-Zeitlimit oder ein verlorener Pfad innerhalb der Epoche behalten diese Frist.
- Prüfungsystem.ping
Vor jedem erneuten Versuch wartet das Telefon eine zufällige Zeit zwischen der Hälfte und dem Ganzen einer Obergrenze, die sich ab 0,25 s verdoppelt und bei 5 s endet, nie über die Frist hinaus. Ein Versuch ohne Socket öffnet zuerst einen, und dieser registriert sich mit seinem Resume-Token.
- SchonfristDESKTOP_RECONNECT_GRACE_SECS = 90
Schließt sich der Socket eines Desktops, sendet das Relay reconnecting mit einer reconnectDeadline und beantwortet Anfragen an diesen Desktop mit -32011. Es sendet online, wenn sich der Desktop erneut registriert, und disconnected, wenn die Frist verstreicht.
- ErgebniscanSendDesktopRPC
Der Desktop gilt erst als verbunden, wenn er selbst auf system.ping antwortet. Das Telefon zeigt ihn als offline an, wenn die Frist verstreicht, sofort bei disconnected oder wenn die Prüfung mangels Netzwerkpfad endet.
Die Wiederherstellung läuft in Epochen, eine pro Desktop, jede mit einer Frist. Innerhalb einer Epoche prüft die App den Desktop mit system.ping und Backoff, und sendet das Relay online, pingt sie sofort. Ein Desktop, der bis zur Frist nicht geantwortet hat oder den das Relay als disconnected meldet, wird als vorübergehend offline angezeigt. Ebenso einer, dessen Prüfung endet, weil das Telefon keinen Netzwerkpfad hat.
NWPathMonitor meldet verlorene Pfade und Schnittstellenwechsel. Ein verlorener Pfad schließt jedes verbundene Relay, lässt dessen ausstehende Aufrufe scheitern und öffnet eine Epoche. Ein Wechsel zwischen WLAN und Mobilfunk schließt das alte Relay und lässt laufende Aufrufe scheitern, bevor die Wiederherstellung genau einen Ersatz öffnet, denn ein migrierter Socket kann weiter Sendungen annehmen, ohne je auf eine Registrierung zu antworten. Der Wechsel verwirft die aktuelle Epoche, deshalb bekommt der Ersatz ein neues Fenster von 90 Sekunden.
Wird die App inaktiv, setzt sie das Timeline-Interesse aus. Geht sie in den Hintergrund, trennt sie jedes Relay innerhalb einer GracefulDisconnect-Hintergrundaufgabe, außer desktopgestützte Medien spielen, was der Audio-Hintergrundmodus erlaubt. Zurück im Vordergrund startet die Wiederherstellung für den aktiven Desktop 1,5 Sekunden später, und ein bestehender Socket bleibt nur, wenn ein Ping innerhalb von 3 Sekunden antwortet. Ein stiller desktop_online-Push startet dieselbe Wiederherstellung sofort.
Event-Annahme vor dem Main Actor
- Snapshot-Platzchat:timeline-updated · event: blocks
Ein Warteplatz pro Session, Run und Thread, und ein neuerer Snapshot ersetzt den wartenden. Über 128 Plätze hinaus wird der älteste verworfen. Snapshots gehen einzeln hinaus, mit 100 ms Pause nach jeder Zustellung.
- Steuer-Eventsrun:status-changed · device-status
Jedes andere Relay-Event wartet in Ankunftsreihenfolge und wird ohne Pause zugestellt, deshalb kann es einen wartenden Snapshot überholen.
- Lease-PrüfungWorkspaceTimelineRelayEventAuthorization
Läuft auf dem Main Actor, wenn ein Timeline-Event zugestellt wird, und verwirft das Event vor dem Dekodieren, wenn sein Desktop und seine Session kein aktuelles Lease halten. Die Prüfung läuft erneut, direkt bevor sich das View-Model ändert.
- Überlastung32 MiB · 64 MiB · 256 Events
Ein Event über 32 MiB, mehr als 64 MiB wartend oder mehr als 256 wartende Steuer-Events verwerfen den Rest dieser Abonnementgeneration. Das Telefon abonniert dann neu und startet die Wiederherstellung des Desktops.
RelayEventIngress nimmt Relay-Events an, bevor irgendeine Arbeit den Main Actor erreicht. Steuer-Events behalten ihre Ankunftsreihenfolge und warten nie hinter Snapshots. Block-Snapshots aus chat:timeline-updated sind UI-Zustand nach bestem Bemühen. Ein neuerer Snapshot für dieselbe Session, denselben Run und Thread ersetzt einen noch wartenden, und Snapshots gehen einzeln hinaus. Das Lease wird geprüft, wenn ein Snapshot auf dem Main Actor zugestellt wird, bevor seine Payload dekodiert ist, deshalb erreichen Inhalte eines Chats, den Sie verlassen haben, nie die Aufbereitung.
Timeline-Inhalte erreichen das Telefon nur, solange ein Chat sichtbar ist. Jede sichtbare Chat-Ansicht hält einen Anspruch, und der Interesse-Koordinator hält ein Lease pro Desktop, für die Basis-Session des neuesten Anspruchs. Ansprüche halten die Übergabe zwischen einem Eltern-Chat und einem Subagent-Viewer sicher, solange beide Ansichten kurz nebeneinander existieren, und ein Anspruch zählt nur für die Vordergrundgeneration, in der seine Ansicht zuletzt ihre Sichtbarkeit bestätigt hat, sodass die Rückkehr in den Vordergrund keine verschwundene Ansicht wiederbeleben kann. Das Lease wird als chat:timeline-interest mit der appSessionId und einem active-Flag gesendet, alle 20 Sekunden erneuert und nach der Registrierung beim Relay erneut ausgestellt. Eine gescheiterte Zustellung wird bis zu 4 Mal versucht, beginnend bei 0,25 Sekunden, mit höchstens 2 Sekunden zwischen den Versuchen und 20 Prozent Jitter.