From 3d595593b9a55efd258285c647b757aafd1e7ec5 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Mon, 14 Sep 2026 20:40:42 +0200 Subject: [PATCH 1/2] Revamp CloudSaveKit integration documentation --- README.md | 8 ++- .../CloudSaveKit.docc/CloudSaveKit.md | 9 +++ .../ExplicitSynchronization.md | 44 +++++++++++++ .../CloudSaveKit.docc/IntegrationGuide.md | 63 +++++++++++++++++++ .../CloudSaveKit.docc/SharingZones.md | 42 +++++++++++++ .../CloudSaveKit.docc/Troubleshooting.md | 57 +++++++++++++++++ 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 Sources/CloudSaveKit/CloudSaveKit.docc/ExplicitSynchronization.md create mode 100644 Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md create mode 100644 Sources/CloudSaveKit/CloudSaveKit.docc/SharingZones.md create mode 100644 Sources/CloudSaveKit/CloudSaveKit.docc/Troubleshooting.md diff --git a/README.md b/README.md index 5034662..3e9f26d 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Add CloudSaveKit to your package dependencies: ```swift .package( url: "https://github.com/thatfactory/cloudsavekit.git", - from: "0.1.1" + from: "0.2.4" ) ``` @@ -90,6 +90,8 @@ try await engine.syncNow() Automatic synchronization should remain enabled in production. Explicit operations complement the system scheduler; they do not replace durable local saves or make offline networking possible. +For a complete integration checklist, including application capabilities, signing verification, durable-state requirements, shared-zone rules, explicit-operation semantics, and diagnostic guidance, read the [CloudSaveKit documentation](https://thatfactory.github.io/cloudsavekit/documentation/cloudsavekit/). + Explicit operations are serialized per engine. An explicit fetch waits for any fetch already active when the request arrives, then requires a fetch generation that began after the request before reporting success. Because CKSyncEngine completes its API call only after related delegate events finish, success is both a freshness barrier and an apply barrier for the configured zone. A post-request automatic fetch may satisfy the barrier; a pre-request fetch cannot. `freshFetchNotObserved` is raised instead of reporting false success if CKSyncEngine returns without a qualifying generation. A configured-zone fetch error also fails its qualifying explicit request. Privacy-safe stage logs report database discovery, configured-zone delivery counts, per-zone completion, and dirty-state transitions without exposing zone or record identities. Call `start()` successfully before any explicit synchronization. `fetchNow()` and `sendNow()` throw `CloudSaveEngineError.notStarted` before startup and `CloudSaveEngineError.hostRecoveryRequired` after a host persistence callback fails. Once the local store is healthy again, call `start()` to rebuild from the last successfully persisted CKSyncEngine checkpoint and the host's current durable pending-change ledger. @@ -128,4 +130,6 @@ CloudSaveKit logs concise synchronization lifecycle information through [AppLogg - Swift 6.4 - Xcode 27 - iOS, macOS, tvOS, watchOS, or visionOS 26+ -- A CloudKit container with CloudKit and Remote Notifications capabilities +- A CloudKit container and deployed record schema for the intended environment +- iCloud with CloudKit, Push Notifications, and the Remote notifications background mode on application targets +- A signed physical-device build whose application entitlements and provisioning profile contain the expected iCloud container and APNs environment diff --git a/Sources/CloudSaveKit/CloudSaveKit.docc/CloudSaveKit.md b/Sources/CloudSaveKit/CloudSaveKit.docc/CloudSaveKit.md index fbd0c6a..6ddd3c5 100644 --- a/Sources/CloudSaveKit/CloudSaveKit.docc/CloudSaveKit.md +++ b/Sources/CloudSaveKit/CloudSaveKit.docc/CloudSaveKit.md @@ -8,6 +8,8 @@ CloudSaveKit wraps Apple's `CKSyncEngine` lifecycle and delegate surface without Create the engine early in application launch, call ``CloudSaveEngine/start()``, and enqueue changes only after their corresponding local transactions succeed. Observe ``CloudSaveEngine/statusUpdates`` to project synchronization state into the host architecture. The current-state stream begins with ``CloudSaveStatus/idle`` and retains only its latest unconsumed value rather than preserving an event history. +Before integrating the engine, follow to configure the application target, signing, local-store boundary, and durable state. For user-initiated refresh behavior, see . For private-zone sharing, see . When server state and device behavior disagree, use . + Automatic synchronization remains enabled by default. Use ``CloudSaveEngine/fetchNow()``, ``CloudSaveEngine/sendNow()``, or ``CloudSaveEngine/syncNow()`` only at user-visible checkpoints where immediate work is useful. Explicit operations are serialized per engine. An explicit fetch waits for pre-request fetch work to drain and requires a post-request fetch generation to complete, including every related host apply, before returning successfully. A newly initialized nil-state engine may reconcile its first matching sign-in while that first explicit fetch is in flight without invalidating the fetch; every later account transition still invalidates current work. Explicit synchronization requires a successful ``CloudSaveEngine/start()`` and raises ``CloudSaveEngineError`` when the engine has not started, host recovery is required, or CKSyncEngine returns without the required fresh generation. CloudSaveKit forwards only records, record deletions, and custom-zone deletions from its configured custom zone. Owned zones may be created and recovered. Shared zones retain their exact owner-qualified identifier and are never recreated by a participant; initial sign-in and account transitions restore the participant's durable record changes without scheduling a zone save, while lost access requires host reconfiguration. If the host cannot persist a sync-engine checkpoint or apply a CloudKit result, the engine cancels the current work and waits for the host to call ``CloudSaveEngine/start()`` after local recovery. Host callback failures are reported as ``CloudSaveFailure/localPersistence``. @@ -16,6 +18,13 @@ CKSyncEngine retains recoverable transport failures and schedules their retries; ## Topics +### Essentials + +- +- +- +- + ### Engine - ``CloudSaveEngine`` diff --git a/Sources/CloudSaveKit/CloudSaveKit.docc/ExplicitSynchronization.md b/Sources/CloudSaveKit/CloudSaveKit.docc/ExplicitSynchronization.md new file mode 100644 index 0000000..3906fad --- /dev/null +++ b/Sources/CloudSaveKit/CloudSaveKit.docc/ExplicitSynchronization.md @@ -0,0 +1,44 @@ +# Explicit Synchronization + +Understand the guarantees and limits of fetch, send, and combined synchronization. + +## Overview + +Automatic synchronization is the normal production mode. Use ``CloudSaveEngine/fetchNow()``, ``CloudSaveEngine/sendNow()``, or ``CloudSaveEngine/syncNow()`` at user-visible checkpoints where immediate progress matters. These methods complement CKSyncEngine's scheduler; they do not replace durable local commits, guarantee network availability, or disable system-scheduled work. + +Call ``CloudSaveEngine/start()`` successfully first. Explicit operations throw ``CloudSaveEngineError/notStarted`` before startup and ``CloudSaveEngineError/hostRecoveryRequired`` after a host persistence failure. + +## Fetch freshness and apply + +An explicit fetch is both a freshness barrier and an apply barrier for the configured zone. It waits for fetch work already active when the request arrives, then requires a fetch generation that began after the request. CKSyncEngine finishes its API call only after the related delegate events and host apply callbacks complete. + +A post-request automatic generation may satisfy the barrier; a pre-request generation cannot. This preserves CKSyncEngine's scheduler while preventing a manual refresh from reporting success merely because older work completed. If no qualifying generation is observed, the operation raises ``CloudSaveEngineError/freshFetchNotObserved``. If the configured zone fails during the qualifying generation, it raises ``CloudSaveEngineError/configuredZoneFetchFailed``. + +Explicit operations are serialized per engine. Repeated taps do not create overlapping explicit engine work. + +## Send reconciliation + +``CloudSaveEngine/sendNow()`` reloads the host's durable pending ledger before sending. It materializes current records through ``CloudSaveClient/record(for:)`` and revalidates asynchronous results against the current engine lifecycle. + +A `nil` materialization means the record no longer exists and is reconciled with the current ledger. A thrown materialization error is a host failure and stops synchronization. After CloudKit acknowledges a save or deletion, CloudSaveKit rereads the durable ledger before removing completed work so a newer mutation of the same record is not erased by an older acknowledgement. + +## Interpreting transfer counts + +An application's operation summary should be interpreted as a snapshot of work attributed to that explicit operation, not as an engine-lifetime counter. CKSyncEngine can run a push-driven scheduled generation immediately before or during a user action. That scheduled generation may apply the remote record before the explicit generation completes, leaving the final manual snapshot with zero downloads even though the visible data is fresh. + +When diagnosing a manual refresh, correlate the complete ordered timeline: + +```text +scheduled or manual generation starts +database reports configured zone changed +configured zone delivers records or deletions +host applies the batch +qualifying generation completes +user-visible operation completes +``` + +Do not infer that no download occurred from the final count alone. + +## Lifecycle invalidation + +Account changes, host failures, cancellation, and engine replacement invalidate stale explicit operations and asynchronous materialization. A newly initialized nil-state engine has one narrow exception: its first matching sign-in event establishes the initial account and may finish reconciliation without invalidating the bootstrap fetch. Every later sign-in, sign-out, or account switch retains full lifecycle invalidation. diff --git a/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md b/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md new file mode 100644 index 0000000..458084f --- /dev/null +++ b/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md @@ -0,0 +1,63 @@ +# Integrating CloudSaveKit + +Configure a host application for durable private or shared CloudKit synchronization. + +## Overview + +CloudSaveKit coordinates CKSyncEngine, but the host remains the source of truth for local data. A correct integration combines application capabilities and signing, a durable ``CloudSaveClient``, an owner-aware zone configuration, and lifecycle orchestration that starts synchronization only after account-scoped persistence is ready. + +## Configure the application target + +Enable iCloud with CloudKit, select the intended container, enable Push Notifications, and enable the Remote notifications background mode. Deploy the record schema to every CloudKit environment the application will use. + +The target's Xcode settings are not proof that a distributed or locally installed binary has the required capabilities. For physical-device testing, inspect both the signed application entitlements and the embedded provisioning profile. They must contain the expected iCloud container and an APNs environment appropriate to the build. Reinstall after changing capabilities or profiles. + +Push capability is operationally important even when the application exposes a manual refresh button. CKSyncEngine relies on CloudKit notifications to discover database changes efficiently. A build without a valid APNs entitlement may upload successfully while a receiving device repeatedly completes fetch calls without discovering a changed zone. + +## Implement the durable client boundary + +Implement ``CloudSaveClient`` in the actor that owns the local store. Its callbacks form a transactional durability boundary: + +- ``CloudSaveClient/pendingChanges()`` returns every locally committed change not yet acknowledged by CloudKit. +- ``CloudSaveClient/record(for:)`` materializes the latest local representation for a pending save. +- ``CloudSaveClient/persist(stateSerialization:)`` stores every opaque CKSyncEngine checkpoint. +- ``CloudSaveClient/applyFetchedChanges(records:deletedRecordIDs:)`` applies one fetched batch atomically. +- ``CloudSaveClient/didSave(records:)`` persists returned system fields before acknowledging uploads. +- ``CloudSaveClient/didDelete(recordIDs:)`` acknowledges remote deletions in the durable ledger. +- Account, conflict, deleted-zone, and failure callbacks update host-owned state and policy. + +Commit application data and its pending ledger entry in one local transaction before calling ``CloudSaveEngine/enqueue(_:)``. Treat the durable ledger, not CKSyncEngine's in-memory queue, as the source of truth across termination and recovery. + +Store server system fields with local records so later updates retain CloudKit change tags. Resolve conflicts using application semantics and base retries on the supplied server record. + +## Preserve checkpoint provenance + +`CKSyncEngine.State.Serialization` is opaque. Store it durably after every callback and restore it through ``CloudSaveConfiguration/stateSerialization``. A host that supports multiple accounts, inventories, database scopes, or zones should bind each serialization to that exact context and reject mismatches before creating an engine. CloudSaveKit cannot infer whether an otherwise valid opaque checkpoint belongs to the host's current domain identity. + +If the host intentionally performs a nil-state recovery, keep it bounded and recoverable. Do not erase user records or pending mutations merely to reset CloudKit state, and do not send pending changes until the recovered checkpoint and topology have been validated. + +## Configure one exact zone + +For an owned zone, use the private database and a ``CloudSaveConfiguration`` initialized with the zone. For a shared zone, use the shared database and the exact owner-qualified zone identifier: + +```swift +let configuration = CloudSaveConfiguration( + database: container.sharedCloudDatabase, + stateSerialization: restoredState, + sharedZoneID: acceptedZoneID +) +``` + +The initializers enforce the database-scope pairing. Preserve the complete `CKRecordZone.ID`, including its owner name. A zone name alone is not sufficient for a shared zone. + +## Start and recover + +Construct the engine only after restoring the matching checkpoint and durable pending ledger, then call ``CloudSaveEngine/start()``. Do not call explicit operations before startup succeeds. + +If a host persistence callback fails, CloudSaveKit invalidates current work and raises ``CloudSaveEngineError/hostRecoveryRequired`` for new explicit operations. Repair the local-store problem and call ``CloudSaveEngine/start()`` again; the engine rebuilds from the last checkpoint that the host successfully persisted and reloads the durable ledger. + +Account transitions invalidate old operations before the host switches account-scoped persistence. The host must restore the new account's ledger and checkpoint in ``CloudSaveClient/handle(accountChange:)``. Never allow one account's pending records or checkpoint to enter another account's engine. + +## Verify the integration + +Test local-first offline creation, relaunch with pending work, conflict resolution, deletion, account transitions, host callback failure and restart, shared-zone revocation, concurrent enqueue and send, and fetched changes applied before user-visible completion. For sharing, perform a two-device test with separate iCloud accounts and verify both upload directions. diff --git a/Sources/CloudSaveKit/CloudSaveKit.docc/SharingZones.md b/Sources/CloudSaveKit/CloudSaveKit.docc/SharingZones.md new file mode 100644 index 0000000..627df6f --- /dev/null +++ b/Sources/CloudSaveKit/CloudSaveKit.docc/SharingZones.md @@ -0,0 +1,42 @@ +# Sharing Custom Zones + +Use one owner-created private zone from both private and shared database views. + +## Overview + +CloudKit zone sharing has two roles: + +- The owner creates a custom zone in the private database and shares that zone. +- A participant accepts the share and accesses the owner's exact zone through the shared database. + +CloudSaveKit keeps those lifecycles distinct. An owned configuration may create or recover its zone. A shared configuration never creates the owner's zone. + +## Create and present a share + +Use ``CloudSaveSharingCoordinator/ensureZoneWideShare(for:)`` with an owned zone identifier. The returned `CKShare` is UI-independent; the host decides how to present the system sharing interface and which permissions to offer. + +Sharing an individual root record is not supported. CloudSaveKit expects a zone-wide share so every synchronized record in the configured zone has one consistent topology. + +## Accept and retain the invitation + +Pass system-provided `CKShare.Metadata` to ``CloudSaveSharingCoordinator/accept(metadata:)``. CloudSaveKit validates that the invitation belongs to the coordinator's container and represents a zone-wide share, then returns the owner-qualified `CKRecordZone.ID`. + +Persist that complete identifier with the host's inventory or account binding. Configure ``CloudSaveConfiguration`` with the shared database and `sharedZoneID`. Never reduce the identity to the zone name: two owners can use the same zone name. + +The host application must receive share metadata through its platform lifecycle and decide which accepted shared zone belongs to its product. ``CloudSaveSharingCoordinator/sharedRecordZones()`` can discover accessible shared zones, but CloudSaveKit does not choose among them. + +## Handle lost access + +If the participant loses access or the shared zone disappears, CloudSaveKit raises ``CloudSaveEngineError/reconfigurationRequired``. Return the application to its sharing or inventory-selection flow. Do not silently create a private zone with the same name; that would fork the data into a different inventory. + +## Test both views + +Use two physical devices signed into separate iCloud accounts: + +1. Create and share the owner's zone. +2. Accept the invitation on the participant account. +3. Upload one unique record from the owner and verify it appears on the participant after automatic or one explicit synchronization. +4. Upload a different record from the participant and verify it appears on the owner. +5. Confirm the participant remains configured with the shared database and the owner's full zone identifier throughout relaunch. + +Simulator share-link handoff and push behavior can differ from physical devices. Use simulators for deterministic application tests, but treat a two-device signed-build run as the significant end-to-end acceptance. diff --git a/Sources/CloudSaveKit/CloudSaveKit.docc/Troubleshooting.md b/Sources/CloudSaveKit/CloudSaveKit.docc/Troubleshooting.md new file mode 100644 index 0000000..9ee2399 --- /dev/null +++ b/Sources/CloudSaveKit/CloudSaveKit.docc/Troubleshooting.md @@ -0,0 +1,57 @@ +# Troubleshooting Synchronization + +Diagnose configuration, server visibility, discovery, delivery, apply, and UI refresh as separate stages. + +## Use an evidence ladder + +For one unique disposable record, establish each boundary in order: + +1. The host committed the local value and durable pending change. +2. CloudSaveKit requested or scheduled the change. +3. The sending device received a successful CloudKit acknowledgement. +4. An authorized reciprocal CloudKit view can observe the record. +5. The receiving engine discovered that its configured zone changed. +6. CKSyncEngine delivered the record or deletion. +7. The host applied the batch transactionally. +8. The user-visible store refreshed after apply. + +Stopping at an earlier boundary can make a later component look broken. For example, successful upload and server visibility do not prove that the receiving build can receive push-driven change discovery. + +## Fetch succeeds but no records arrive + +If explicit fetch generations complete while the known server record remains absent locally: + +- Verify that the engine uses the intended container, environment, database scope, and full owner-qualified zone identifier. +- Verify that the restored checkpoint belongs to that same account and topology. +- Inspect the signed application entitlements and embedded provisioning profile for the expected iCloud container and APNs environment. +- Confirm Push Notifications and the Remote notifications background mode are enabled. +- Confirm the receiving account still has access to the shared zone. +- Look for database-change and configured-zone delivery events, not only the outer fetch completion. + +A successful fetch API call does not independently prove that a stale or mismatched checkpoint discovered the expected zone. Likewise, a configured-zone dirty flag of `false` is not proof that the server contains no newer records. + +## Data appears but the manual result says zero + +CKSyncEngine may perform a scheduled fetch immediately before the explicit generation. If logs show database discovery, zone delivery, and host apply before manual completion, the final explicit operation can correctly report zero transferred records because the scheduled generation already applied them. Treat operation counts as attribution snapshots and use the ordered timeline. + +## Shared data forks into a private zone + +Verify that participants use `sharedCloudDatabase` and the accepted owner-qualified zone identifier. Never recover a missing shared zone by creating a same-named zone in the private database. Lost shared access requires host reconfiguration. + +## Startup or recovery fails + +``CloudSaveEngineError/notStarted`` means no successful ``CloudSaveEngine/start()`` has completed. ``CloudSaveEngineError/hostRecoveryRequired`` means a client persistence callback failed; repair the local store and restart the engine. ``CloudSaveEngineError/reconfigurationRequired`` means the configured shared topology is no longer available. + +Do not fix checkpoint problems by deleting user data, pending mutations, or server system fields. Bind opaque state to host-owned provenance and make any nil-state recovery explicit, bounded, and observable. + +## Read CloudSaveKit logs + +CloudSaveKit uses subsystem `com.thatfactory.cloudsavekit`, category `sync`, and prefix ☁️. It reports privacy-safe generations, database discovery, configured-zone delivery counts, dirty-state transitions, and failures without record or zone identifiers. + +Useful acceptance evidence includes a changed configured zone, delivered modification or deletion counts, successful zone completion, and a host-side confirmation that the batch committed. Framework console noise unrelated to these boundaries should not be treated as a package failure without a corresponding CloudSaveKit or host error. + +Never add record contents, identifiers, share URLs, account values, or credentials to logs merely to make correlation easier. + +## Avoid fragile repairs + +Do not add arbitrary sleeps, unbounded polling, repeated engine recreation, destructive token resets, or duplicate private zones. These approaches obscure which boundary failed and can create data divergence. Use deterministic lifecycle tests and one controlled physical-device reproduction instead. From 8c796714ba29c5d2e42ab25195d608dfb6c8f6a4 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Mon, 14 Sep 2026 20:48:37 +0200 Subject: [PATCH 2/2] Correct CloudSaveClient lifecycle documentation --- Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md b/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md index 458084f..5c089b0 100644 --- a/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md +++ b/Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md @@ -23,7 +23,7 @@ Implement ``CloudSaveClient`` in the actor that owns the local store. Its callba - ``CloudSaveClient/persist(stateSerialization:)`` stores every opaque CKSyncEngine checkpoint. - ``CloudSaveClient/applyFetchedChanges(records:deletedRecordIDs:)`` applies one fetched batch atomically. - ``CloudSaveClient/didSave(records:)`` persists returned system fields before acknowledging uploads. -- ``CloudSaveClient/didDelete(recordIDs:)`` acknowledges remote deletions in the durable ledger. +- ``CloudSaveClient/didDelete(recordIDs:)`` acknowledges successfully sent local pending deletions after CloudKit reports them in `SentRecordZoneChanges`; fetched deletions arrive through ``CloudSaveClient/applyFetchedChanges(records:deletedRecordIDs:)``. - Account, conflict, deleted-zone, and failure callbacks update host-owned state and policy. Commit application data and its pending ledger entry in one local transaction before calling ``CloudSaveEngine/enqueue(_:)``. Treat the durable ledger, not CKSyncEngine's in-memory queue, as the source of truth across termination and recovery. @@ -56,7 +56,7 @@ Construct the engine only after restoring the matching checkpoint and durable pe If a host persistence callback fails, CloudSaveKit invalidates current work and raises ``CloudSaveEngineError/hostRecoveryRequired`` for new explicit operations. Repair the local-store problem and call ``CloudSaveEngine/start()`` again; the engine rebuilds from the last checkpoint that the host successfully persisted and reloads the durable ledger. -Account transitions invalidate old operations before the host switches account-scoped persistence. The host must restore the new account's ledger and checkpoint in ``CloudSaveClient/handle(accountChange:)``. Never allow one account's pending records or checkpoint to enter another account's engine. +Account transitions invalidate old operations before the host switches account-scoped persistence. In ``CloudSaveClient/handle(accountChange:)``, the host switches or validates account-scoped local persistence and restores the new account's durable pending ledger. The callback cannot replace serialized CKSyncEngine state in the running engine: state serialization is supplied only when constructing ``CloudSaveConfiguration``, and CKSyncEngine performs its own account-change state reset. Never allow one account's pending records or host-bound checkpoint provenance to enter another account's persistence context. ## Verify the integration