Eine einzige Quelle der Wahrheit: SwiftData, MCP, iCloud
Bei Get Bananas können drei Aufrufer in dieselbe Einkaufsliste schreiben. Ein Mensch, der in der iOS-App auf Zeilen tippt. Apple Intelligence, das eine Siri-Anfrage über ein AppIntent weiterleitet. Eine Claude-Code-Sitzung, die über stdio ein MCP-Tool aufruft. Im Kopf des Benutzers ist die Liste ein einziges verbindliches Ding; die Frage ist, wo sie im Speicher liegt und welcher Aufrufer gewinnt, wenn die Aufrufer sich widersprechen.
Der Synthese-Beitrag hat die drei Oberflächen einer iOS-App benannt: Mensch, Apple Intelligence, Agent. Jede Oberfläche muss denselben Domänenzustand lesen und schreiben. Aus dieser Anforderung entsteht genau jener Architekturfehler, der in zu vielen Apps ausgeliefert wird: Jede Oberfläche bekommt ihren eigenen Speicher, die Oberflächen driften auseinander, und Benutzer sehen je nach zuletzt berührter Oberfläche drei verschiedene Versionen ihrer Liste. Was trägt, ist eine einzige Quelle der Wahrheit mit ausdrücklichen Synchronisationswegen zwischen den Oberflächen und dem Substrat.
Dieser Beitrag benennt die möglichen Substrate, die Konfliktregeln, die jedes davon erzwingt, und die Architektur, die trägt, wenn alle drei Aufruferklassen schreiben dürfen. Als Beispiel dient der tatsächliche Aufbau von Get Bananas: SwiftData für den App-internen Zustand, eine JSON-Datei in iCloud Drive für die prozessübergreifende Synchronisation und ein MCP-Server, der dieselbe Datei von außerhalb der iOS-Sandbox liest und schreibt.1
Kurzfassung
- Drei Substrate greifen ineinander: SwiftData (prozessintern, schnell, schematypisiert), iCloud Drive (prozessübergreifend, dateibasiert, synchronisierbar),
NSUbiquitousKeyValueStore(geräteübergreifend, Schlüssel-Wert, nur kleine Nutzlasten). - Entscheiden Sie pro Domäne, welches Substrat die Quelle der Wahrheit ist. Die Konfliktauflösung folgt zwangsläufig aus dieser Wahl und ist kein separates Problem.
- Die drei Oberflächen (Mensch, Apple Intelligence, Agent) sprechen jeweils über die Domänenschicht mit dem Substrat. In der Domänenschicht liegt die Konfliktregel.
- Ein
lastModified-Zeitstempel auf jeder veränderlichen Entität ist das billige Grundwerkzeug der Konfliktauflösung; „der letzte Schreiber gewinnt“ ist die billige Regel. Apps, die stärkere Garantien brauchen, bezahlen sie mit ausdrücklicher Merge-Logik. - Ein MCP-Server außerhalb des App-Prozesses kann SwiftData nicht lesen. Die Brücke ist eine Serialisierungsschicht (JSON-Datei in iCloud Drive, App-Group-Container usw.), mit der sowohl der App-interne SwiftData-Leser als auch der prozessexterne MCP-Server sprechen können.
Die drei Substrate
Apple stellt ausgelieferten iOS-Apps drei native Persistenzsubstrate zur Verfügung, die in prozess- und geräteübergreifenden Mustern auftauchen. Jedes hat eine eigene Form; wer sie ohne Plan mischt, erzeugt genau das Driftproblem.
SwiftData. Prozessinterner persistenter Speicher auf Basis von Core Data.2 Schnell, schematypisiert, über @Query abfragbar, in das Observation-System von SwiftUI integriert. Der Speicher gehört dem App-Prozess. App-Erweiterungen (Widgets, Intents, Share-Erweiterungen) können sich über eine App Group und ausdrückliche Konfiguration einen SwiftData-Container teilen; ein beliebiger externer MCP-Prozess, der auf dem Rechner eines Entwicklers außerhalb des Signierungskontexts der App läuft, kann nicht gefahrlos in den SwiftData-Container greifen. Zeilen sind über PersistentIdentifier (prozessintern) oder natürliche Schlüssel mit @Attribute(.unique) (prozess- und geräteübergreifend) adressierbar. Migrationen laufen deklarativ über VersionedSchema und MigrationPlan (behandelt in Der wahre Preis von SwiftData ist Schemadisziplin).
iCloud Drive. Dateibasierte geräteübergreifende Synchronisation, zugänglich über FileManager-URLs im iCloud-Container des Benutzers.3 Die Dateien erscheinen auf jedem Gerät, an dem der Benutzer angemeldet ist. Die Konfliktauflösung findet auf Dateiebene statt: iCloud verfolgt gleichzeitige Änderungen über NSFileVersion, und die App liest das Konfliktprotokoll, um zu entscheiden, was bleibt. Dateien in iCloud Drive sind von außerhalb des iOS-App-Prozesses adressierbar: Ein MCP-Server auf dem Mac kann dieselbe JSON-Datei öffnen, die die iOS-App liest. Genau dieses Substrat macht die MCP-Anbindung von Get Bananas möglich.
NSUbiquitousKeyValueStore. Geräteübergreifender Schlüssel-Wert-Speicher. Apples aktuelle öffentliche Grenzwerte liegen bei 1 MB gesamt pro App, 1 MB pro Wert, 1024 Schlüsseln und 128 UTF-16-Zeichen pro Schlüssel, dazu gedrosselte Schreibraten.4 Die Konfliktauflösung ist eingebaut: Das System serialisiert Schreibvorgänge und benachrichtigt bei Änderungen alle Geräte. Geeignet für kleinen, selten geänderten Zustand (Einstellungen, zuletzt gewählter Tab, ein Zähler); ungeeignet für Daten mit hoher Schreibrate oder Lasten, bei denen die Drosselung zum Engpass wird. Return nutzt den Speicher für geräteübergreifenden Timer-Zustand (Timer auf dem iPhone starten, auf der Apple Watch sehen); behandelt in Fünf Apple-Plattformen, drei geteilte Dateien.
Das vierte Substrat, CloudKit, ist die naheliegend wirkende Wahl, die die Apps dieses Clusters für die prozessübergreifende MCP-Anbindung ausdrücklich verworfen haben. CloudKit bietet starke geräteübergreifende Synchronisation mit konfliktbewussten Datensätzen, und Apple liefert mit CloudKit JS und den CloudKit Web Services durchaus Wege, damit Nicht-Apple-Umgebungen mit öffentlichen und privaten CloudKit-Datenbanken sprechen können. Der ehrliche Kompromiss betrifft die Integrationskosten, nicht die Machbarkeit: Ein Node.js-MCP-Server, der eine private CloudKit-Datenbank erreichen will, muss Authentifizierung über die CloudKit Web Services, Schemadefinitionen und Request-Signierung aufbauen — spürbarer Entwicklungsaufwand im Vergleich zu „eine JSON-Datei öffnen“. Get Bananas hat sich für iCloud Drive plus JSON-Datei entschieden, weil der MCP-Server ein Node.js-Prozess ist, der dieselben Daten lesen und schreiben muss, die auch die iOS-App sieht, und gewöhnlicher Dateizugriff der Weg des geringsten Widerstands ist.1
Die Entscheidung: Welches Substrat hält die Wahrheit
Die Frage lautet nicht, welches Substrat man verwendet. Die Frage lautet, welches Substrat für welche Domäne die Quelle der Wahrheit ist. Die übrigen Substrate cachen sie, spiegeln sie oder halten sich heraus.
Die Entscheidungsmatrix, die sich für die Apps dieses Clusters im Produktivbetrieb bewährt hat:
| Form der Domäne | Quelle der Wahrheit | Begründung |
|---|---|---|
| Einstellungen, Präferenzen, einfache Flags | NSUbiquitousKeyValueStore |
Geräteübergreifende Synchronisation läuft automatisch; Kollisionen werden serialisiert; die kleine Nutzlast passt |
| Gerätespezifischer flüchtiger Zustand | UserDefaults (ohne Sync) |
Gerätelokal; sollte eine Neuinstallation auf einem anderen Gerät nicht überleben |
| Prozessinterne abfragbare Sammlung | SwiftData | Schnelles @Query, SwiftUI-Observation, schematypisiert; nur prozessintern |
| Prozessinterne Sammlung, die externe Prozesse erreichen muss | JSON-Datei in iCloud Drive (Export auf die Festplatte) | Sowohl der SwiftData-Leser unter iOS als auch der externe MCP-Server können die Datei lesen |
| Große benutzerbezogene Inhalte (Fotos, Audio, Dokumente) | iCloud Drive (pro Datei) | Die iCloud des Benutzers ist der natürliche Speicher; CloudKit kann für reichere Synchronisation darüberliegen |
| Geräteübergreifender Sitzungszustand (Timer läuft auf dem iPhone, sichtbar auf der Watch) | NSUbiquitousKeyValueStore |
Passt in den Größenrahmen; braucht die geräteübergreifende Push-Semantik |
Aus der Entscheidung folgt die Konfliktregel. Für die Brücke zwischen lokaler App und externem MCP hat SwiftData zwischen Prozessen keine eingebaute Konfliktauflösung; schreiben zwei Aufrufer in dieselbe Zeile, gewinnt das letzte try context.save(). SwiftData mit CloudKit im Rücken und aktivierter Persistent History kann reichere geräteübergreifende Semantik tragen, doch diese Oberfläche liegt auf der iOS-Seite und hilft im externen Node.js-MCP-Fall nicht weiter. iCloud Drive legt Konflikte als NSFileVersion-Einträge offen; die App muss sie durchlaufen und einen Sieger bestimmen. NSUbiquitousKeyValueStore bringt die Konfliktauflösung auf Wertebene bereits mit.
Die Architektur von Get Bananas
Der tatsächliche Aufbau von Get Bananas:
┌────────────────────────────────────┐
│ User's mental model │
│ "my shopping list" │
└─────────────────┬──────────────────┘
│
┌────────────┴───────────┐
│ │
┌───────▼────────┐ ┌───────▼─────────┐
│ iOS app │ │ MCP server │
│ (Get Bananas) │ │ (Node.js) │
└───────┬────────┘ └───────┬─────────┘
│ │
┌───────────┴────────┐ │
│ │ │
┌──────▼──────┐ ┌────────▼──────────┐ │
│ SwiftData │ │ iCloud Drive │◀──┘
│ (in-process) │◀──▶│ shopping_list. │
│ │ │ json │
└──────────────┘ └───────────────────┘
In-app reads/writes flow through SwiftData.
Cross-process reads/writes flow through the JSON file.
An iCloud sync layer (iCloudBackupManager) reconciles the two.
Die Architektur folgt drei Regeln.
SwiftData ist die Quelle der Wahrheit für prozessinterne Abfragen. Die iOS-App liest bei jedem UI-Rendering, bei jeder @Query-gestützten Liste und bei jeder Suche aus SwiftData. Schreibvorgänge gehen zuerst durch SwiftData; der Model Context speichert; SwiftUI rendert neu.
Die JSON-Datei ist die Quelle der Wahrheit für den prozessübergreifenden Zustand. Immer wenn die iOS-App in SwiftData speichert, exportiert ein iCloud-Backup-Manager den aktuellen Zustand in eine JSON-Datei im iCloud-Drive-Container des Benutzers. Immer wenn der MCP-Server schreibt, schreibt er in dieselbe JSON-Datei. Die Datei ist die Brücke.
Beim Start der iOS-App und nach jedem prozessübergreifenden Schreibvorgang läuft ein Synchronisationsdurchgang. Die heute ausgelieferte Synchronisationslogik (SyncManager.applyExport) behandelt das JSON-Backup bei jedem Durchgang als maßgeblich: Sie liest die JSON-Datei, ordnet jede Zeile über die UUID einem SwiftData-Datensatz zu, überschreibt vorhandene Zeilen mit den Werten aus dem Backup, ergänzt Zeilen, die das Backup hat und SwiftData nicht, und löscht Zeilen, die SwiftData hat und das Backup nicht (abgesichert gegen Beschädigung, damit eine leere Backup-Datei nicht die lokale Datenbank leert). Die Regel lautet das Backup gewinnt zum Synchronisationszeitpunkt, nicht zeilenweise „der letzte Schreiber gewinnt“ nach Zeitstempel. Zusammen mit dem erneuten Export der iOS-App nach jedem Speichern konvergiert das System in der Praxis schnell: Wer zuletzt geschrieben hat, hat die JSON-Datei erzeugt, die die nächste Synchronisation liest.
Die Architektur tauscht Komplexität gegen prozessübergreifende Reichweite. Eine reine SwiftData-App braucht davon nichts; eine App ohne MCP-Server braucht die JSON-Brücke nicht; eine App ohne geräteübergreifende Synchronisation braucht den Abgleich nicht. Get Bananas braucht alle drei, weil alle drei Aufruferklassen (Mensch über iOS, Apple Intelligence über App Intents unter iOS, Agent über MCP vom Mac eines Entwicklers) dieselbe Einkaufsliste berühren.
Der Ausbaupfad: zeilenweise „der letzte Schreiber gewinnt“
Die ausgelieferte Regel „das Backup gewinnt zum Synchronisationszeitpunkt“ ist billig und trägt im Fall eines einzelnen Benutzers mit jeweils einem aktiven Schreiber. Schwierig wird es, wenn iOS-App und MCP-Server kurz hintereinander in die JSON-Datei schreiben: Wer zuletzt geschrieben hat, überschreibt die Änderungen der Gegenseite — auch bei Zeilen, die gar nicht in Konflikt standen. Abgefedert wird das heute dadurch, dass die iOS-App nach jedem SwiftData-Speichern erneut exportiert und die JSON-Datei so grob am aktuellsten App-internen Zustand hält. Im eingeschwungenen Zustand ist das in Ordnung; im echt nebenläufigen Fall kann Arbeit verloren gehen.
Der billigste Ausbau ist zeilenweises „der letzte Schreiber gewinnt“, geschlüsselt auf eine Spalte lastModified: Date?. Das Modell ShoppingItem trägt das Feld bereits aus Gründen der Migrationssicherheit (behandelt in Der wahre Preis von SwiftData ist Schemadisziplin), doch der JSON-Export und der MCP-Server serialisieren und berücksichtigen es derzeit nicht. Würde man lastModified durch den Export und durch applyExport durchreichen, änderte sich die Merge-Regel von „das Backup gewinnt“ zu „die neuere Zeile gewinnt“:
- Beide Seiten haben einen Wert, einer ist neuer. Der neueste gewinnt. Die Zeile der Gegenseite wird aktualisiert.
- Beide Seiten haben einen Wert, sie sind gleich alt. Entschieden wird über den Primärschlüssel oder über die Oberfläche (bei Gleichstand gewinnt die iOS-App, um die jüngste Interaktion in der App zu bevorzugen).
- Eine Seite hat einen Wert, die andere nicht. Die Seite mit dem Wert gewinnt.
- Keine Seite hat einen Wert. Beide Zeilen stammen aus der Zeit vor
lastModified. Der Abgleich stempeltDate()für das nächste Mal.
Die Regel ist billig, leicht nachzuvollziehen und in etwa 1 % der Fälle falsch (gleichzeitige Änderungen an verschiedenen Feldern derselben Zeile). Bei einer Einkaufsliste spielt dieses 1 % keine Rolle; bei einem Dokumenteneditor sehr wohl. Apps, die stärkere Garantien brauchen, setzen feldweises Merging, CRDTs oder Operational Transforms auf diese Basis auf; Get Bananas hat diese Komplexität bislang nicht gebraucht, weshalb zeilenweises LWW auf der Roadmap steht und reicheres Merging nicht.
Die drei Aufruferklassen und das Substrat
Die Aufruferklassen aus Drei Oberflächen auf die Substratentscheidungen abgebildet:
Die menschliche Oberfläche schreibt über SwiftData. Tippt jemand in der iOS-App auf ein Kästchen, läuft das über die SwiftUI-Schicht in eine Domänenfunktion, die die SwiftData-Zeile ändert, lastModified = Date() auf dem Modell stempelt und den Model Context speichert. Der iCloud-Export schreibt den aktuellen Zustand in die JSON-Datei. Der MCP-Server nimmt den neuen Zustand beim nächsten Lesen auf.
Die Apple-Intelligence-Oberfläche schreibt über SwiftData. Ein über Siri aufgerufenes AppIntent läuft im iOS-App-Prozess und erreicht dieselbe Domänenfunktion, die auch die menschliche Oberfläche nutzt. Der SwiftData-Zustand ändert sich, lastModified des Modells wird aktualisiert, und der JSON-Export hält den neuen Zustand fest.
Die Agentenoberfläche schreibt über die JSON-Datei. Ein MCP-Tool-Aufruf aus einer Claude-Code-Sitzung auf einem Mac ändert die JSON-Datei direkt (mit Dateisperre, um nebenläufige Schreibvorgänge der iOS-App abzufangen). Beim nächsten Start oder Synchronisationsdurchgang der iOS-App liest SyncManager.applyExport die Datei, geht die Einträge nach UUID durch, aktualisiert auf beiden Seiten vorhandene Zeilen mit den Werten aus dem Backup, ergänzt Zeilen, die das Backup enthält, und löscht Zeilen, die es auslässt (abgesichert gegen ein leeres Backup). Ausgeliefert wird die Regel das Backup gewinnt zum Synchronisationszeitpunkt; der Ausbaupfad ergänzt lastModified in der JSON-Datei, damit die Regel auf die neuere Zeile gewinnt umgestellt werden kann.
Die Asymmetrie ist real und gewollt. Die menschliche und die Apple-Intelligence-Oberfläche laufen beide im iOS-App-Prozess und nutzen SwiftData direkt. Die Agentenoberfläche läuft außerhalb des iOS-App-Prozesses und nutzt die JSON-Datei, weil das das Substrat ist, das sie erreichen kann. Der Abgleich hält beide Hälften zusammen.
Wann dieses Muster die falsche Antwort ist
Einige Fälle, in denen die JSON-Brücke fehl am Platz ist.
Daten mit hoher Schreibrate. Ein Live-Dokumenteneditor mit vielen Änderungen pro Sekunde kann nicht bei jedem Schreibvorgang die gesamte Sammlung in eine JSON-Datei serialisieren. Die richtige Antwort sind Operational Transforms oder CRDTs gegen ein echtes Backend.
Anforderungen an starke Konsistenz. Ein Hauptbuch für Finanztransaktionen verträgt kein „letzter Schreiber gewinnt“ auf der JSON-Datei. Die richtige Antwort ist CloudKit (oder eine serverseitige Datenbank) mit ausdrücklicher Transaktionssemantik.
Zusammenarbeit mehrerer Benutzer, die sich gegenseitig in Echtzeit beim Bearbeiten zusehen. Die Synchronisation über iCloud Drive ist letztlich konsistent (eventual consistency), nicht echtzeitfähig. Wer die App auf einem Gerät schließt und auf einem anderen öffnet, sieht den synchronisierten Zustand; wer den Cursor einer anderen Person durch ein Dokument wandern sehen will, sieht ihn nicht. Die richtige Antwort ist ein Framework für Echtzeitzusammenarbeit (yjs, automerge oder eine eigene WebSocket-Schicht).
Fälle, in denen Agent und Benutzer verschiedene Identitäten sind. Das Get-Bananas-Muster setzt voraus, dass der Agent (der MCP-Aufrufer) und der menschliche Benutzer (der Benutzer der iOS-App) dieselbe Person sind, nur eben prozessübergreifend am Werk. Handelt der Agent im Auftrag einer anderen Identität (eine geteilte Liste, ein Administrator, ein automatisierter Bot), ist die JSON-Datei im iCloud Drive des Benutzers das falsche Substrat; dann braucht es Mehrbenutzer-Persistenz mit ausdrücklicher Authentifizierung.
Das Muster passt zum Fall eines einzelnen Benutzers, der letztlich konsistent und prozessübergreifend arbeitet. Die meisten Apps mit MCP-Anbindung sind genau dieser Fall; manche nicht.
Was ich anders bauen würde
Drei Muster, die die Apps dieses Clusters entweder ausgeliefert haben oder sich rückblickend gewünscht hätten.
Machen Sie die JSON-Serialisierung ausdrücklich, nicht implizit. Die erste Version von Get Bananas exportierte jeden SwiftData-Schreibvorgang in einem Save-Hook nach JSON. Die zweite Version machte den Export zu einem ausdrücklichen Schritt, den die App aufruft, sobald sich der Zustand stabilisiert hat. Das reduzierte überflüssige Schreibvorgänge und machte klar, wann der prozessübergreifende Zustand veröffentlicht worden war. Ein impliziter Hook, der bei jeder Änderung speichert, erzeugt bei jeder nicht trivialen Sammlung zu viel I/O.
Versionieren Sie das Schema der JSON-Datei. Die JSON-Datei hat ihr eigenes Schema, unabhängig vom VersionedSchema von SwiftData. Ändert sich das SwiftData-Schema (etwa durch ein neues Feld), muss die JSON-Serialisierung nachziehen. Der billige Kniff: ein Feld schemaVersion: Int an den Anfang der JSON-Datei setzen; der Abgleich liest es und wendet die passende Auslegung an. Ohne Versionierung läuft eine v2-iOS-App, die eine von einem alten MCP-Server geschriebene v1-JSON-Datei liest, in stille Datenkorruption.
Sperren Sie die JSON-Datei, setzen Sie nicht auf Koordination. iOS-App und MCP-Server schreiben beide in die JSON-Datei. Ohne einen NSFileCoordinator (prozessintern, auf der iOS-Seite) und eine Dateisperre (prozessextern, auf dem Entwicklungsrechner) können nebenläufige Schreibvorgänge eine beschädigte Datei erzeugen. Der MCP-Server von Get Bananas verwendet eine Dateisperre im Stil von flock auf der JSON-Datei; die iOS-App nutzt für ihre Schreibvorgänge NSFileCoordinator; in der Praxis ist die Datei selten umkämpft, aber der Sicherheitsgurt kostet fast nichts.
Was das Muster für Apps ab iOS 26 bedeutet
Drei Erkenntnisse.
-
Wählen Sie pro Domäne eine Quelle der Wahrheit. Die übrigen Substrate cachen, spiegeln oder halten sich heraus. SwiftData für prozessinterne Abfragen, JSON in iCloud Drive für prozessübergreifende Brücken,
NSUbiquitousKeyValueStorefür kleinen geräteübergreifenden Zustand. Die Konfliktauflösung ist eine nachgelagerte Folge dieser Wahl. -
lastModifiedplus „der letzte Schreiber gewinnt“ ist der billige Grundfall. Die meisten Apps brauchen keine stärkeren Garantien. Jenes 1 % der Fälle, das feldweises Merging oder CRDTs verlangt, ist teuer nachzurüsten; zahlen Sie den Preis erst, wenn die Datenform ihn erzwingt. -
Der Abgleich ist das tragende Bauteil. Wenn SwiftData und die JSON-Datei sich widersprechen, entscheidet der Abgleich. Er läuft beim App-Start, nach prozessübergreifenden Schreibvorgängen und nach iCloud-Synchronisationsereignissen. Die Regeln sind einfach; die Disziplin besteht darin, ihn tatsächlich laufen zu lassen.
Der vollständige Apple-Ecosystem-Cluster: typisierte App Intents für die Apple-Intelligence-Oberfläche; MCP-Server für die Agentenoberfläche; die Frage des Routings zwischen beiden; Foundation Models für App-interne LLM-Funktionen auf dem Gerät; die Unterscheidung zwischen LLM zur Laufzeit und LLM im Werkzeug; die Synthese der drei Oberflächen einer iOS-App; Live Activities für den Zustandsautomaten auf dem iOS-Sperrbildschirm; der Vertrag der watchOS-Laufzeit auf der Apple Watch; SwiftUI von innen als Substrat der menschlichen Oberfläche; RealityKits räumliches Denkmodell für visionOS-Szenen; SwiftData-Schemadisziplin für die Persistenz; Liquid-Glass-Muster für die visuelle Schicht; Auslieferung auf mehreren Plattformen für die geräteübergreifende Reichweite. Der Knotenpunkt ist die Apple-Ecosystem-Serie. Für den größeren Zusammenhang von iOS und KI-Agenten siehe den Leitfaden zur iOS-Agentenentwicklung.
FAQ
Warum nicht CloudKit für die prozessübergreifende Synchronisation?
CloudKit bietet starke geräteübergreifende Synchronisation mit konfliktbewussten Datensätzen, und mit CloudKit JS beziehungsweise den CloudKit Web Services erreichen auch Nicht-Apple-Stacks eine private CloudKit-Datenbank. Die Einschränkung sind die Integrationskosten: Ein Node.js-MCP-Server mit CloudKit muss die Authentifizierung von CloudKit (Server-zu-Server-Schlüssel oder Tokens auf Benutzerebene), Schemadeklarationen und signierte Requests bewältigen. iCloud Drive plus JSON-Datei ist gewöhnlicher Dateizugriff — der Universalübersetzer. CloudKit ist richtig, wenn das Team bereit ist, die Integrationskosten zu tragen, und die stärkere Synchronisations- und Konfliktsemantik von CloudKit will; die JSON-Brücke ist richtig, wenn „eine Datei öffnen“ für die Datenform genügt.
Wie geht man mit Konflikten um, wenn zwei Aufrufer gleichzeitig schreiben?
Ausgeliefert wird bei Get Bananas die Regel „das Backup gewinnt zum Synchronisationszeitpunkt“: SyncManager.applyExport geht die Einträge nach UUID durch und überschreibt lokale Zeilen mit den Werten aus dem Backup, abgesichert dagegen, dass ein leeres Backup gute lokale Daten löscht. Der Ausbaupfad ist zeilenweises „der letzte Schreiber gewinnt“, geschlüsselt auf lastModified — ein Feld, das das Modell bereits trägt, das aber über die JSON-Brücke noch nicht serialisiert wird. Damit ließen sich die rund 99 % der Konflikte auflösen, bei denen eine Seite tatsächlich neuer ist; das verbleibende 1 % (gleichzeitige Änderungen an verschiedenen Feldern derselben Zeile) ist für die bisherigen Apps selten genug, um auf feldweises Merging oder CRDTs zu verzichten. Apps mit höheren Konsistenzanforderungen legen reicheres Merging darüber.
Wo passt SwiftData hin, wenn iCloud Drive die Quelle der Wahrheit für den prozessübergreifenden Zustand ist?
SwiftData ist die Quelle der Wahrheit für prozessinterne Abfragen. Die iOS-App liest SwiftData bei jedem UI-Rendering, bei jedem @Query, bei jeder Suche. SwiftData ist schnell, schematypisiert und in das Observation-System von SwiftUI integriert. Schreibt die iOS-App, geht die Änderung zuerst nach SwiftData und wird dann in die JSON-Datei exportiert. Die JSON-Datei ist die Quelle der Wahrheit für prozessübergreifende Lesevorgänge (die Sicht des MCP-Servers); SwiftData ist die Quelle der Wahrheit für prozessinterne Lesevorgänge (die Sicht der iOS-Oberfläche). Ausgerichtet bleiben beide durch den Abgleich.
Und NSUbiquitousKeyValueStore für die Einkaufsliste selbst?
NSUbiquitousKeyValueStore ist auf 1 MB gesamt pro App und 1 MB pro Wert gedeckelt, schreibt gedrosselt und serialisiert über ein Wörterbuch mit 1024 Schlüsseln. Eine Einkaufsliste mit Hunderten Einträgen plus Historie mag rein nach Bytes hineinpassen, doch Änderungen einzelner Einträge durch die Drosselung zu schieben, ist die falsche Form; Aktualisierungen ganzer Sammlungen konkurrieren mit allem anderen, was die App speichert, um das Ratenbudget. Das richtige Substrat für Sammlungen ist entweder SwiftData (prozessintern) oder iCloud Drive (prozessübergreifend). Heben Sie NSUbiquitousKeyValueStore für kleinen, selten geänderten Schlüssel-Wert-Zustand auf: Einstellungen, zuletzt gewählter Tab, ein Zähler, eine Feature-Flag-Überschreibung.
Woran erkenne ich, welches Substrat für eine neue Domäne in meiner App passt?
Drei Fragen der Reihe nach. Erstens: Muss irgendetwas außerhalb des iOS-App-Prozesses diese Domäne lesen oder schreiben? Wenn ja, brauchen Sie iCloud Drive (dateibasiert, gewöhnlicher Dateizugriff), CloudKit (über Apples Frameworks oder die CloudKit Web Services aus Nicht-Apple-Stacks) oder einen Server, den Sie kontrollieren. Wenn nein, ist SwiftData die Standardwahl. Zweitens: Muss das über die Geräte des Benutzers hinweg synchronisieren? Wenn ja, muss das Substrat das unterstützen (iCloud Drive kann es, SwiftData nicht, solange es nicht mit iCloud-Synchronisation gekoppelt ist). Drittens: Wie groß ist die Nutzlast und wie oft ändert sie sich? Klein und selten geändert lebt in NSUbiquitousKeyValueStore; alles andere braucht eine echte Persistenzschicht.
Quellen
-
Analyse des Autors in Zwei Agenten-Ökosysteme, eine Einkaufsliste, 29. April 2026, sowie die iCloud-Drive-JSON-Synchronisationsschicht des Get-Bananas-Projekts in
Banana List/iCloudBackupManager.swift. Die Architektur koppelt SwiftData mit einer JSON-Datei im iCloud-Drive-Container des Benutzers, die ein externer MCP-Server liest und schreibt. ↩↩ -
Apple Developer, “SwiftData” und “Adding and editing persistent data in your app”. Das Makro
@Model, die@Attribute-Constraints und der Bezug zumNSManagedObjectModelvon Core Data. Analyse des Autors in Der wahre Preis von SwiftData ist Schemadisziplin behandeltVersionedSchemaundMigrationPlanfür sichere Schemaevolution. ↩ -
Apple Developer, “Synchronizing documents in the iCloud environment”. Dateibasierte geräteübergreifende Synchronisation, Konfliktauflösung über
NSFileVersionund dieNSFileCoordinator-API für sichere prozessinterne Schreibvorgänge auf gemeinsam genutzten Dateien. ↩ -
Apple Developer, “NSUbiquitousKeyValueStore”. Geräteübergreifender Schlüssel-Wert-Speicher. Apples aktuelle Grenzwerte: 1 MB gesamt pro App, 1 MB pro Wert, 1024 Schlüssel, 128 UTF-16-Zeichen pro Schlüssel, gedrosselte Schreibrate. Analyse des Autors in Fünf Apple-Plattformen, drei geteilte Dateien behandelt das geräteübergreifende Timer-Muster, das Return mit dieser API ausliefert. ↩