mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
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:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user