feat(axiom-data): add CloudKit zone-wide sharing

Document CKShare(recordZoneID:) alongside root-record sharing: a two-model decision table, the blank-data pitfall for apps that read by enumerating a whole zone, and zone constraints (one share per zone, CKRecordNameZoneWideShare, the accept-side database-changes -> zone-changes fetch). Also fix an adjacent compile bug — CKDatabase.add(_:) is synchronous, so the root-record example now uses modifyRecords(saving:deleting:).

Modernize the docs reference page to the current template (When to Use / Example Prompts / What's Covered index / Documentation Scope / relationship-explained Related) with both sharing models first-class.

Verified against CloudKit SDK headers + sosumi.ai.
This commit is contained in:
Charles Wiltgen
2026-06-28 14:58:58 -07:00
parent 4507261fe2
commit 65d2f50572
2 changed files with 109 additions and 118 deletions
@@ -516,25 +516,49 @@ func application(_ application: UIApplication,
## Sharing Records
### Create a Share
CloudKit has two sharing models. Pick by how the app reads the shared data.
| Model | API | What's shared | Use when |
|-------|-----|---------------|----------|
| Hierarchical (root-record) | `CKShare(rootRecord:)` | Root record + its `parent`-linked descendants | You share one document/object and traverse its hierarchy from the root |
| Zone-wide | `CKShare(recordZoneID:)` | Every record in the zone | You read/write by enumerating the whole zone, so records aren't tied to a single root |
**Pitfall**: root-record sharing only shares records reachable through `parent` references from the root. If the app instead enumerates the entire zone (no root hierarchy), the participant sees an empty data set. Use zone-wide sharing for that access pattern.
### Create a Share (Root-Record)
```swift
// Share a record with other users
// Share a record and its parent-linked descendants
let record = try await privateDatabase.record(for: recordID)
// Record must be in a custom zone (not default zone)
// Record must be in a custom zone (not the default zone)
let share = CKShare(rootRecord: record)
share[CKShare.SystemFieldKey.title] = "Shared Task List"
share.publicPermission = .none // Invite-only
// Save both the record and share together
let operation = CKModifyRecordsOperation(
recordsToSave: [record, share],
recordIDsToDelete: nil
)
try await privateDatabase.add(operation)
// Save the root record and share together, atomically
try await privateDatabase.modifyRecords(saving: [record, share], deleting: [])
```
### Share an Entire Zone (Zone-Wide)
```swift
// Every record in the zone is shared no root record
// Custom zones in the private database have the .zoneWideSharing
// capability by default; the default zone cannot be shared.
let share = CKShare(recordZoneID: customZone.zoneID)
share[CKShare.SystemFieldKey.title] = "Household Inventory"
share.publicPermission = .none
// No root record to batch save the share on its own
try await privateDatabase.save(share)
```
**Constraints**:
- A zone and its records can take part in **only one** share — zone-wide and per-record shares are mutually exclusive within the same zone.
- The zone-wide share's record name is the well-known constant `CKRecordNameZoneWideShare`.
- On accept, CloudKit copies the records into a new zone in the participant's shared database. Get the new zone ID with `CKFetchDatabaseChangesOperation`, then read records with `CKFetchRecordZoneChangesOperation` (there is no root record to fetch).
### Present Sharing UI
```swift
+76 -109
View File
@@ -1,25 +1,72 @@
---
name: cloudkit-ref
description: Modern CloudKit sync — SwiftData integration, CKSyncEngine, database APIs, conflict resolution
description: Modern CloudKit sync and sharing — SwiftData integration, CKSyncEngine, database APIs, root-record + zone-wide CKShare, conflict resolution
skill_type: reference
---
# CloudKit Reference
Comprehensive CloudKit reference for database-based iCloud storage and sync using modern APIs.
CloudKit reference for database-backed iCloud storage, sync, and sharing. Covers the three modern sync approaches (SwiftData + CloudKit, CKSyncEngine, raw CloudKit), the two record-sharing models, and conflict resolution.
## Overview
For file/document iCloud (a ubiquitous container) see [icloud-drive-ref](/reference/icloud-drive-ref) — a different problem. For sync *failures* and conflict debugging, see [cloud-sync-diag](/diagnostic/cloud-sync-diag).
CloudKit is for **structured data sync** (records with relationships), not simple file sync. Three modern approaches: SwiftData + CloudKit (easiest), CKSyncEngine (custom persistence), or raw CloudKit APIs.
## When to Use This Reference
**Based on**: CKSyncEngine (WWDC 2023), SwiftData CloudKit integration (WWDC 2023-2024), CloudKit Console (WWDC 2024)
Use this reference when you're:
## Three Approaches
- Choosing between SwiftData + CloudKit, CKSyncEngine, and raw CloudKit APIs
- Syncing structured records (with relationships) across a user's devices
- Sharing records or an entire zone with other iCloud users (collaboration)
- Deciding between root-record and zone-wide sharing
- Resolving save conflicts (`CKError.serverRecordChanged`, save policies)
- Setting up custom zones, subscriptions, and change tracking
- Monitoring CloudKit error rates and quota in the Console
### 1. SwiftData + CloudKit (Recommended, iOS 17+)
## Example Prompts
**When to Use**: Modern apps with SwiftData models
Questions you can ask Claude that will draw from this reference:
**Limitations**: Private database only, automatic sync, no `@Attribute(.unique)`
- "Should I use SwiftData + CloudKit or CKSyncEngine for my app?"
- "How do I share a record with another iCloud user?"
- "Should I use root-record (`CKShare(rootRecord:)`) or zone-wide (`CKShare(recordZoneID:)`) sharing?"
- "I shared my data but the person I invited sees a blank/empty list — why?"
- "How do I share an entire CloudKit zone?"
- "How do I handle `CKError.serverRecordChanged` conflicts?"
- "How do I fetch only the changes since my last sync?"
## What's Covered
### Sync Approaches
- SwiftData + CloudKit — `ModelConfiguration(cloudKitDatabase:)`; private-DB only, no `@Attribute(.unique)`
- CKSyncEngine — automatic fetch/upload for custom persistence (SQLite/GRDB/JSON)
- Raw CloudKit — `CKContainer`, `CKDatabase`, `CKRecord`, `CKRecordZone`, `CKModifyRecordsOperation`
### Databases & Scopes
- Private / Public / Shared scopes — access, SwiftData support, use case
- `privateCloudDatabase`, `publicCloudDatabase`, `sharedCloudDatabase`
### Zones & Change Tracking
- Custom zones vs the default zone; `CKRecordZone`
- `CKFetchDatabaseChangesOperation`, `CKFetchRecordZoneChangesOperation`, server change tokens
### Subscriptions
- `CKSubscription` (query / zone / database), silent push, `CKModifySubscriptionsOperation`
### Sharing
- **Root-record (hierarchical)** `CKShare(rootRecord:)`; shares a record plus its `parent`-linked descendants
- **Zone-wide** `CKShare(recordZoneID:)`; shares *every* record in the zone, for apps that read by enumerating the whole zone
- The decision: an enumerate-the-zone app needs zone-wide, or the invitee sees an empty data set (root-record only shares the rooted hierarchy)
- `UICloudSharingController`, participant permissions, `CKAcceptSharesOperation`, the `CKRecordNameZoneWideShare` constant
### Conflict Resolution
- Save policies (`.ifServerRecordUnchanged`), `CKError.serverRecordChanged`, server/client record merge
### Monitoring
- CloudKit Console — error rate, latency percentiles, quota usage, alerts
## Key Patterns
### SwiftData + CloudKit (the easy path)
```swift
let container = try ModelContainer(
@@ -30,113 +77,33 @@ let container = try ModelContainer(
)
```
**See Also**: `axiom-data` skill for details
### 2. CKSyncEngine (Modern, iOS 17+)
**When to Use**: Custom persistence (SQLite, GRDB, JSON)
**Advantages**: Automatic fetch/upload, conflict handling, account changes
### Choosing a sharing model
```swift
let config = CKSyncEngine.Configuration(
database: CKContainer.default().privateCloudDatabase,
stateSerialization: loadState(),
delegate: self
)
let syncEngine = try CKSyncEngine(config)
// Hierarchical: share one record and its parent-linked children.
let share = CKShare(rootRecord: rootRecord)
// Zone-wide: share the whole zone. Use this when the app reads by
// enumerating the zone rather than walking a root hierarchy with
// root-record sharing, such an app exposes a blank data set to the invitee.
let zoneShare = CKShare(recordZoneID: customZone.zoneID)
```
**Modern replacement** for manual CKDatabase operations
## Documentation Scope
### 3. Raw CloudKit APIs (Legacy)
This page documents the `cloudkit-ref` skill — database-backed CloudKit (records, sync, sharing). The comprehensive patterns and code live in the skill, which Claude loads automatically.
**When to Use**: Only if CKSyncEngine doesn't fit (rare)
- For an automated audit of an existing CloudKit/iCloud integration, run the [icloud-auditor](/agents/icloud-auditor)
- For file/document iCloud rather than record sync, see [icloud-drive-ref](/reference/icloud-drive-ref)
**Core Types**:
- CKContainer — Entry point
- CKDatabase — Public/private/shared
- CKRecord — Data record
- CKRecordZone — Logical grouping
## Related
## Database Scopes
- [swiftdata](/skills/persistence/swiftdata) SwiftData models that sync via `ModelConfiguration(cloudKitDatabase:)`
- [cloud-sync-diag](/diagnostic/cloud-sync-diag) diagnose sync failures and conflict errors when CloudKit misbehaves
- [icloud-drive-ref](/reference/icloud-drive-ref) file/document iCloud sync, distinct from record sync
- [storage](/skills/persistence/storage) choosing CloudKit vs iCloud Drive vs local storage
- [icloud-auditor](/agents/icloud-auditor) automated scan for entitlement, CKError-coverage, and account-change gaps
| Scope | Access | SwiftData | Use Case |
|-------|--------|-----------|----------|
| **Private** | User only | ✅ | Personal data |
| **Public** | All users | ❌ | Shared content |
| **Shared** | Invited users | ❌ | Collaboration |
## Resources
## Common Patterns
### SwiftData CloudKit Sync
```swift
// Automatic sync for SwiftData models
@Model
class Task {
var title: String
var dueDate: Date
// Syncs automatically with ModelConfiguration
}
```
### CKSyncEngine Delegate
```swift
extension Manager: CKSyncEngineDelegate {
func handleEvent(_ event: CKSyncEngine.Event,
syncEngine: CKSyncEngine) async {
switch event {
case .fetchedRecordZoneChanges(let changes):
applyChanges(changes)
case .sentRecordZoneChanges(let changes):
handleSent(changes)
case .accountChange(let change):
handleAccountChange(change)
default:
break
}
}
}
```
### Conflict Resolution
```swift
// CKSyncEngine handles conflicts automatically
// Or with raw APIs:
operation.savePolicy = .ifServerRecordUnchanged
// Handle CKError.serverRecordChanged
if error.code == .serverRecordChanged {
let merged = mergeRecords(
server: error.serverRecord,
client: error.clientRecord
)
try await database.save(merged)
}
```
## CloudKit Console Monitoring
**Access**: https://icloud.developer.apple.com/dashboard
**Monitor**:
- Error rates, latency (p50, p95, p99)
- Request volume, bandwidth
- Quota usage
**Set Alerts**:
- High error rate (>5%)
- Quota approaching limit (>80%)
## Use This Skill When
- Implementing structured data sync
- Choosing SwiftData+CloudKit vs CKSyncEngine
- Setting up public/private/shared databases
- Debugging CloudKit sync
- Monitoring CloudKit performance
**Related**: swiftdata, storage, icloud-drive-ref, cloud-sync-diag
**Docs**: /cloudkit, /cloudkit/ckshare, /cloudkit/cksyncengine, /cloudkit/ckrecordzone