---
title: markbattistella/swiftdatasync
framework: Swift Package Catalog
role: article
path: packages/markbattistella/swiftdatasync
---

# markbattistella/swiftdatasync

`SwiftDataSync` connects an app-owned SwiftData store to any number of shared CloudKit record zones. SwiftData remains the durable local source of truth while `CKSyncEngine` delivers a transactionally maintained outbox to the owner's private database or a participant's shared dat

## Requirements

| Platform | Minimum | | --- | --- | | Swift | 6.0 | | iOS / iPadOS / Mac Catalyst | 17.0 | | macOS | 14.0 | | tvOS | 17.0 | | watchOS | 10.0 | | visionOS | 1.0 |

## Installation

Add the package with Swift Package Manager:

```swift dependencies: [   .package(     url: "https://github.com/markbattistella/SwiftDataSync",     from: "1.0.0"   ) ] ```

Then add `SwiftDataSync` to the app target.

## App configuration

Every consuming app still owns its CloudKit contract. Enable:

- iCloud with CloudKit - Remote notifications - A CloudKit container - `CKSharingSupported` in the app's Info property list

Deploy the app's CloudKit schema before shipping.

Create one configuration. Unlike a single-zone setup, this doesn't carry a fixed zone name of its own - an app can track any number of zones side by side, each named however its own model needs:

```swift let configuration = SwiftDataSyncConfiguration(   containerIdentifier: "iCloud.com.example.Tasks",   appGroupIdentifier: "group.com.example.Tasks",   stateKeyPrefix: "tasks.sync",   shareTitle: "Shared tasks",   appName: "Tasks",   dataName: "workspace" ) ```

## SwiftData adapter

The package deliberately does not reflect arbitrary `@Model` classes into CloudKit. Each app implements `SwiftDataSyncStore` so record fields, deletions, migrations, and merge policy remain explicit.

```swift @MainActor final class TaskCloudStore: SwiftDataSyncStore {   let modelContext: ModelContext

// Return durable outbox rows, materialise CKRecords, apply fetched records,   // preserve conflicts, and commit through this same ModelContext. } ```

Every pending change's `collectionID` (on `SwiftDataSyncPendingChange`) tells the engine which zone it belongs to - the outbox row should be inserted or updated in the same SwiftData transaction as the app model being changed. This guarantees that terminating the app between a local save and a network request cannot silently lose the upload.

Create and retain the engine:

```swift let syncEngine = SwiftDataSyncEngine(   configuration: configuration,   store: TaskCloudStore(modelContext: modelContainer.mainContext) ) ```

Provision a zone this device owns - safe to call repeatedly (e.g. once per owned zone at every launch):

```swift let zoneID = configuration.ownedZoneID(named: "Workspace-\(workspaceID)") syncEngine.ensureZoneExists(zoneID, collectionID: workspaceID) ```

Call `reconcileOutbox()` after committing a local mutation. Call `fetchChangesNow()` for an explicit user-requested refresh.

## Sharing

Prepare a specific zone's zone-wide share:

```swift let sharing = SwiftDataSyncSharingCoordinator(syncManager: syncEngine) await sharing.prepareShare(for: zoneID, title: "My workspace") ```

Present `sharing.activeShare(for: zoneID)` with `UICloudSharingController`. Forward accepted share metadata from the app or scene delegate - pass the `collectionID` this zone belongs to, if the app can determine it (e.g. by encoding it in the zone's own name):

```swift syncEngine.adoptSharedZone(   metadata.share.recordID.zoneID,   collectionID: workspaceID ) ```

The owner always writes to a custom zone in their private database. CloudKit exposes that zone in each accepted participant's shared database; the package selects the correct database per zone from that zone's own persisted role - a device can own some zones and participate in others at the same time. `syncEngine.role(for:)`, `.zoneID(forCollection:)`, and `.collectionID(for:)` let the app move between a zone and the collection it represents in either direction.

## Data-safety contract

- Local SwiftData is authoritative. - Network delivery is eventually consistent. - Failed changes remain in the durable outbox. - A participant's private data is protected before adopting a share, scoped to the one collection being adopted - every other collection this device already owns or participates in is untouched. - A revoked share invokes the app's recovery hook for just that one collection instead of deleting local data. - CloudKit conflicts are passed to the app for explicit preservation and merge.

## CloudSyncKit

`SwiftDataSync` is separate from [`CloudSyncKit`](https://github.com/markbattistella/CloudSyncKit). `CloudSyncKit` observes `NSPersistentCloudKitContainer` events. `SwiftDataSync` performs custom-record-zone syncing and cross-account sharing with `CKSyncEngine`.

## Licence

`SwiftDataSync` is released under the MIT licence.

## Package Metadata

Repository: markbattistella/swiftdatasync

Default branch: main

README: README.md
