Zum Artikel springen
PlanToCodeDocsApp herunterladen

HandbuchiOS-App

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 PaketWas es enthältHängt ab von
App-TargetDrei 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.
VibeUIJeder 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.
CoreEin 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.
VibeUIFormattingMarkdown-Texthelfer: der Pfadklassifizierer, der Linkifier für Inline-Referenzen und der Normalisierer für nummerierte Listen.Nichts.
VibeUITestsDas 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-StammverzeichnisBeginnen 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

Ein gesperrtes Telefon kann die Geräte-ID nach dem ersten Entsperren lesen, das App-JWT aber nicht
Ein gesperrtes Telefon kann die Geräte-ID nach dem ersten Entsperren lesen, das App-JWT aber nichtVier Momente nacheinander, nicht maßstäblich: ein Neustart, das erste Entsperren, das Sperren des Telefons und das erneute Entsperren. Die Geräte-ID ist bis zum ersten Entsperren nicht lesbar und bleibt danach lesbar, auch während das Telefon gesperrt ist. Das App-JWT und die Resume-Tokens sind nur lesbar, solange das Telefon entsperrt ist. Ein Start vor dem ersten Entsperren wartet auf dem Ladebildschirm, bis geschützte Daten verfügbar werden. Ein Start bei gesperrtem Telefon liest die Geräte-ID und startet Core, behält die gespeicherte Anmeldung, weil das JWT nicht lesbar ist, und prüft sie nach dem Entsperren erneut.
Neustart
erstes Entsperren
gesperrt
entsperrt
Geräte-IDIdentität des Telefons
App-JWTund Resume-Tokens
AppStart, Anmeldung
nicht lesbar
lesbar, auch im gesperrten Zustand
nicht lesbar
lesbar
nicht lesbar
lesbar
Start wartet: keine Geräte-ID, Ladebildschirm bleibt
Geräte-ID lesbar, Core startet, Anmeldung geprüft
Start im gesperrten Zustand: Core startet, JWT nicht lesbar, Anmeldung bleibt
Entsperren: Anmeldung erneut geprüft
  • 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.

MethodenStandard-Zeitlimit
files.readContent8 Sekunden.
Dateibäume, Run-Liste und -Abruf, Nutzungslimits, Auth-Profile, Sprachvokabular und -kontext, Verlaufszustand90 Sekunden.
Session- und Projektlisten und -Aktualisierungen, Abgleich, bestehende Sessions, Abbrechen und Löschen, Diffs, Befehlsausgabe, Webseiten als Markdown, Binär- und Medienlesevorgänge, Anhang-Uploads60 Sekunden.
Alles andere30 Sekunden.
Chat-Richtlinie: erste Timeline, ältere Seite, Outbox-Bestätigung115, 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

Relay und Telefon geben einem getrennten Desktop dieselbe Frist von 90 Sekunden
Relay und Telefon geben einem getrennten Desktop dieselbe Frist von 90 SekundenIn diesem Beispiel schließt sich die Verbindung des Desktops zum Relay bei 0 Sekunden. Das Relay hält den Platz des Desktops 90 Sekunden lang frei, meldet dem Telefon reconnecting zusammen mit dieser Frist und beantwortet jeden Ping an ihn mit -32011. Das Telefon öffnet eine Epoche der Wiederherstellung, die zum selben Zeitpunkt endet, und pingt mit Backoff: vier Versuche in etwa 2 Sekunden, danach einer alle 2,5 bis 5 Sekunden. Bei 30 Sekunden registriert sich der Desktop erneut, das Relay sendet online, das Telefon pingt sofort, und sobald der Desktop selbst antwortet, gilt er als verbunden. Wäre der Desktop weggeblieben, hätte das Relay bei 90 Sekunden disconnected gesendet und das Telefon ihn als offline angezeigt.
DesktopBeispiel: 30 s weg
RelaySchonfrist
TelefonWiederherstellung
nicht verbunden
jeder Ping: -32011
Schonfrist liefe weiter
pingt mit Backoff
Epoche liefe weiter
reconnecting, Frist in 90 s
registriert sich neu
online
Desktop beantwortet den Ping
verbunden
4 Versuche in etwa 2 s, danach einer alle 2,5 bis 5 s
Versucheerste 6 s, vergrößert
disconnected
offline
0 s
15 s
30 s
45 s
60 s
75 s
90 s
  • 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

Ein wartender Snapshot kann ersetzt werden, und das Lease wird bei der Zustellung geprüft
Ein wartender Snapshot kann ersetzt werden, und das Lease wird bei der Zustellung geprüftEin Beispiel mit fünf Relay-Events innerhalb von 260 Millisekunden, während Sie den Chat wechseln. Snapshot T1 für Chat T wird sofort zugestellt. T2 kommt in der Pause von 100 Millisekunden nach dieser Zustellung an und wartet, bis T3 ihn ersetzt. Ein run:status-changed-Event wartet nicht und wird vor T3 zugestellt. Sie öffnen Chat S bei 105 Millisekunden, deshalb hält die Session von T3 bei dessen Zustellung bei 150 Millisekunden kein Lease mehr, und der Main Actor verwirft ihn vor dem Dekodieren. S1 für Chat S kommt später an, hält das Lease und wird dekodiert und angewendet.
kommt anNetzwerk-Queue
wartetRelayEventIngress
LeaseInteresse-Koordinator
zugestelltMain Actor
Lease: Chat T
Lease: Chat S
Wechsel zu Chat S
T1
T2
T3
S1
run:status-changed
wartet nicht
durch T3 ersetzt
T1 gezeigt
T3 verworfen: kein Lease, nie dekodiert
S1 gezeigt
100 ms Pause
0 ms
100 ms
200 ms
300 ms
  • 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.