Skip to content

About

Synchronizes durable local data with CloudKit through CKSyncEngine. ☁️

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Swift Version Xcode Version Platforms SPM DocC License CI Release

CloudSaveKit

A reusable CloudKit account, private-bootstrap, and CKSyncEngine coordinator for app-owned local data. ☁️

flowchart LR
    LocalStore["Host local store"] --> Client["CloudSaveClient"]
    Client <--> Engine["CloudSaveEngine"]
    Engine <--> CloudKit["Private or shared CloudKit database"]
Loading

CloudSaveKit owns CloudKit synchronization mechanics while the host application remains responsible for its local persistence, record schema, merge semantics, and user experience. It deliberately has no dependency on SwiftData, Core Data, Redux, or SwiftUI.

Responsibilities

  • Restore and persist opaque CKSyncEngine state.
  • Create and recover an owned custom zone without recreating participant-owned shared zones.
  • Create, accept, and discover private zone-wide shares through a UI-independent coordinator.
  • Batch pending record saves and deletions within CloudKit limits.
  • Schedule automatic synchronization and expose explicit fetch, send, and combined sync operations.
  • Forward fetched changes, account events, saved system fields, and semantic conflicts to the host.
  • Classify failures into privacy-safe values suitable for application state.
  • Verify the current account and invalidate personal sessions before switching local persistence.
  • Provision a canonical private manifest conditionally, reconcile concurrent creators, and fetch exact asset records.
  • Preserve independent record, zone, operation, and host-persistence failures until their exact recovery conditions succeed.

Quick start

Add CloudSaveKit to your package dependencies:

.package(
    url: "https://github.com/thatfactory/cloudsavekit.git",
    from: "0.3.1"
)

Implement CloudSaveClient in the actor that owns your local database. For one personal collection per account, use CloudSavePrivateCoordinator: it constructs the container, observes account changes, verifies identity, and conditionally provisions a generic manifest. Its CloudSavePrivateSession permanently binds that account and zone to your client. See the private collection integration guide for a complete neutral-schema example and the offline trust contract.

Personal sessions revalidate incoming manifest identities and drain admitted host callbacks during retirement. Await retirement before reading a checkpoint for a replacement session over the same store. Configured-zone network errors retain their transport classification so a verified, initialized cache can remain usable offline; a failed fetch never establishes initial-empty readiness.

For Apple TV consumers, a visible profile switch does not by itself prove that CloudKit supplied that profile's account. The troubleshooting guide gives a privacy-safe account/store comparison and a bounded system-profile recovery path. CloudSaveKit binds the account actually verified by CloudKit; the host must test two real profiles before claiming isolation.

let coordinator = CloudSavePrivateCoordinator(
    containerIdentifier: "iCloud.com.example.notebook"
)
// Observe coordinator.accountEvents before querying or opening a local store.
let accountSnapshot = try await coordinator.refreshAccount()

Persist an account-qualified manifest identity, reject mismatched checkpoint provenance, and use the coordinator's bootstrapManifest result to create a session. Await start() and fetchNow() before treating a new local cache as empty. Commit local records, media associations, and their pending mutations before enqueueing; an upload failure does not roll back a successful local Save. A lost trusted manifest or zone is an integrity failure, and personal sessions never recreate it or invoke destructive zone-deletion callbacks.

The existing configurable engine and sharing APIs remain available. For a host that intentionally manages mutable account bindings and automatic owned-zone recovery, configure a database and zone directly:

let container = CKContainer(identifier: "iCloud.com.example.game")
let zone = CKRecordZone(zoneName: "GameSave")
let configuration = CloudSaveConfiguration(
    database: container.privateCloudDatabase,
    stateSerialization: restoredState,
    zone: zone
)
let engine = CloudSaveEngine(
    configuration: configuration,
    client: localStore
)

try await engine.start()

For a share participant, preserve the exact owner-qualified zone ID and configure the shared database:

let configuration = CloudSaveConfiguration(
    database: container.sharedCloudDatabase,
    stateSerialization: restoredSharedState,
    sharedZoneID: discoveredZoneID
)

Use CloudSaveSharingCoordinator to create or fetch a private zone-wide share, accept system-provided share metadata, and discover shared zones. The host remains responsible for presenting UICloudSharingController and deciding which shared zone belongs to its product. A missing or revoked shared zone raises CloudSaveEngineError.reconfigurationRequired; CloudSaveKit never creates a private replacement.

Persist every state value received by CloudSaveClient.persist(stateSerialization:) and restore it through CloudSaveConfiguration.stateSerialization. Also return every locally durable unsent change from pendingChanges(): this is what lets the engine recover correctly after termination, relaunch, zone recreation, and iCloud account changes.

After committing local data, enqueue its durable CloudKit change:

await engine.enqueue([.save(recordID)])

Use syncNow() when the application must explicitly fetch, merge, and send before continuing:

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.

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.

sendNow() reloads that ledger before sending. This makes the host the source of truth if CKSyncEngine discarded a semantic failure or if a previously persisted checkpoint still contains a change the host has since acknowledged. Host-ledger reads and mutations are serialized and versioned; a snapshot from before a host commit is rejected, while app enqueues that race with a suspended read are replayed in their original order. After a successful send, CloudSaveKit rereads the host ledger before removing completed work; a newer save of the same record therefore remains pending instead of being erased by the older acknowledgement. A nil record(for:) result means the record no longer exists and uses the same reconciliation, while a thrown materialization error stops synchronization and requires host recovery. Materialization cancellation remains under CKSyncEngine's cancellation lifecycle and does not invalidate the host. Record batches are also revalidated after asynchronous materialization and rejected as a whole if an account or engine lifecycle changed while they were being built.

Opaque CKSyncEngine checkpoint writes are serialized with host-failure lifecycle invalidation. A host callback failure blocks new work and advances the lifecycle immediately, while explicit start() recovery waits for every earlier checkpoint write and the failed engine's teardown. A replacement engine therefore cannot start from a checkpoint that an older engine later regresses through actor reentrancy.

Known iCloud account transitions advance the engine lifecycle and block new synchronization before the host switches account-scoped persistence. The gate remains closed until the new account's durable pending ledger is restored, invalidating every pre-transition snapshot and explicit operation without allowing previous-account work to enter the new account. The sole exception is the first matching sign-in reported by a newly initialized nil-state engine: that event establishes the engine's initial account and may finish reconciliation without invalidating the explicit fetch that triggered it. The host still validates the account, and every later sign-in, sign-out, or switch retains full invalidation. Owned configurations also restore their zone save, while shared configurations restore only record changes because a participant must never create the owner's zone. Transitions clear operation and recovery state scoped to the previous account. Sign-out immediately publishes the cleared current status instead of leaving the previous account's pending or failed projection buffered.

statusUpdates is a current-state projection, not an event history. It begins with .idle and retains only the latest unconsumed status so an absent or slow observer cannot accumulate an unbounded buffer.

Failure and retry policy

CloudSaveKit leaves temporary transport, service, authentication, throttling, and cancellation failures to CKSyncEngine's scheduler. Explicit methods and their host-ledger preflight reads still throw their underlying cancellation so the caller can finish its immediate workflow, but routine retryable errors do not become durable attention-required state.

Semantic and permanent failures are tracked independently:

  • A record failure clears only when that record is saved, its deletion is acknowledged, or the host removes it from the durable pending ledger.
  • A zone failure clears only after that zone succeeds.
  • An explicit operation failure clears only after a later matching operation completes and every older overlapping operation of that kind has drained.
  • A host persistence failure stops the engine and blocks all synchronization until a successful start().

This follows Apple's CKSyncEngine contract: the framework schedules and retries recoverable transport work, while the application persists engine state and resolves semantic record failures.

Conflict handling

CloudSaveKit forwards serverRecordChanged to CloudSaveClient.resolve(conflict:). A retry record must be based on the supplied server record so it retains the current CloudKit change tag. The host may accept the server value, return a merged retry record, or preserve the conflict for a user decision. Accepting the server must clear the corresponding item from the host's durable pending ledger when applying the server record; CloudSaveKit removes the same pending save from CKSyncEngine.

Logging

CloudSaveKit logs concise synchronization lifecycle information through AppLogger, using subsystem com.thatfactory.cloudsavekit, category sync, and prefix ☁️. It never logs record contents or identifiers.

Requirements

  • Swift 6.4
  • Xcode 27
  • iOS, macOS, tvOS, watchOS, or visionOS 26+
  • 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

About

Synchronizes durable local data with CloudKit through CKSyncEngine. ☁️

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Contributors

Languages