---
name: cloudkit
description: Apple CloudKit framework for iOS/macOS/watchOS/tvOS development. Use for iCloud data persistence, multi-device sync, CKSyncEngine implementation, CKContainer/CKDatabase/CKRecord operations, conflict resolution, error handling, subscriptions, and CloudKit best practices. Triggers on CloudKit questions, iCloud sync implementation, CKRecord CRUD, zone management, or cross-device data synchronization in Swift/SwiftUI apps.
---
# CloudKit Framework Skill
CloudKit is Apple's framework for iCloud data persistence with up to **1PB public storage** and automatic cross-device sync.
## Code Review Checklist
When reviewing CloudKit code, verify:
- [ ] Account status checked before private/shared database operations
- [ ] Custom zones used (not default zone) for production data
- [ ] All CloudKit errors handled with `retryAfterSeconds` respected
- [ ] `serverRecordChanged` conflicts handled with proper merge logic
- [ ] `CKErrorPartialFailure` parsed for individual record errors
- [ ] Batch operations used (`CKModifyRecordsOperation`) not individual saves
- [ ] Large binary data stored as `CKAsset` (records have 1MB limit)
- [ ] Record keys type-safe (enums) not string literals
- [ ] UI updates dispatched to main thread from callbacks
- [ ] `CKAccountChangedNotification` observed for account switches
- [ ] Subscriptions have unique IDs to prevent duplicates
- [ ] CKShare uses custom zone (sharing requires custom zones)
- [ ] CKSyncEngine state token cached on every `.stateUpdate` event
- [ ] Schema deployed to production before App Store release
### Review Output Format
Report issues as: `[FILE:LINE] ISSUE_TITLE`
Examples:
- `[SyncManager.swift:45] Missing CKSyncEngine state token persistence`
- `[DataStore.swift:89] Unhandled serverRecordChanged conflict`
- `[CloudKit.swift:156] Individual saves instead of batch operation`
## Quick Start
```swift
import CloudKit
// Initialize container and database
let container = CKContainer.default() // or CKContainer(identifier: "iCloud.your.bundle.id")
let privateDB = container.privateCloudDatabase
let publicDB = container.publicCloudDatabase
let sharedDB = container.sharedCloudDatabase
```
## Core Architecture
| Component | Purpose |
|-----------|---------|
| **CKContainer** | Top-level entry point (1 per app typically) |
| **CKDatabase** | Storage layer (private/public/shared) |
| **CKRecordZone** | Logical grouping of records in private DB |
| **CKRecord** | Single data item (like a dictionary) |
| **CKRecord.ID** | Unique identifier (recordName + zoneID) |
| **CKAsset** | Binary data (images, files) |
| **CKReference** | Relationships between records |
| **CKSubscription** | Push notification triggers |
## CKSyncEngine (iOS 17+) — Recommended Approach
CKSyncEngine dramatically simplifies sync. See [references/cksyncengine.md](references/cksyncengine.md) for complete implementation guide.
### Minimal Setup
```swift
import CloudKit
class SyncManager: CKSyncEngineDelegate {
private var engine: CKSyncEngine!
private let container = CKContainer(identifier: "iCloud.your.bundle.id")
init() {
let config = CKSyncEngine.Configuration(
database: container.privateCloudDatabase,
stateSerialization: loadCachedState(), // nil if first launch
delegate: self
)
engine = CKSyncEngine(config)
}
// MARK: - Delegate Methods
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
switch event {
case .stateUpdate(let update):
// CRITICAL: Always cache the state token
saveCachedState(update.stateSerialization)
case .accountChange(let change):
handleAccountChange(change)
case .fetchedRecordZoneChanges(let changes):
// Server → Local: Process incoming data
for modification in changes.modifications {
saveLocally(modification.record)
}
for deletion in changes.deletions {
deleteLocally(deletion.recordID)
}
case .sentRecordZoneChanges(let sent):
// Confirm successful uploads, handle failures
for failure in sent.failedRecordSaves {
handleSaveFailure(failure)
}
default: break
}
}
func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
// Local → Server: Provide records to upload
let pending = syncEngine.state.pendingRecordZoneChanges.filter {
context.options.scope.contains($0)
}
return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: pending) { recordID in
return getLocalRecord(for: recordID)
}
}
// MARK: - Queue Changes
func queueSave(_ record: CKRecord) {
engine.state.add(pendingRecordZoneChanges: [.saveRecord(record.recordID)])
}
func queueDelete(_ recordID: CKRecord.ID) {
engine.state.add(pendingRecordZoneChanges: [.deleteRecord(recordID)])
}
}
```
## CRUD Operations (Direct API)
For non-CKSyncEngine apps or public database. See [references/crud-operations.md](references/crud-operations.md).
```swift
// CREATE
let record = CKRecord(recordType: "Note")
record["title"] = "My Note"
record["content"] = "Hello CloudKit"
let saved = try await database.save(record)
// READ
let recordID = CKRecord.ID(recordName: "unique-id")
let fetched = try await database.record(for: recordID)
// UPDATE
fetched["content"] = "Updated content"
let updated = try await database.save(fetched)
// DELETE
try await database.deleteRecord(withID: recordID)
// QUERY
let predicate = NSPredicate(format: "title BEGINSWITH %@", "My")
let query = CKQuery(recordType: "Note", predicate: predicate)
let (results, _) = try await database.records(matching: query)
```
## Error Handling
See [references/error-handling.md](references/error-handling.md) for complete error codes.
```swift
do {
try await database.save(record)
} catch let error as CKError {
switch error.code {
case .serverRecordChanged:
// Conflict! Resolve using serverRecord
let serverRecord = error.serverRecord
resolveConflict(local: record, server: serverRecord)
case .networkFailure, .networkUnavailable, .serviceUnavailable:
// Transient - retry with backoff
let retryAfter = error.retryAfterSeconds ?? 30
scheduleRetry(after: retryAfter)
case .quotaExceeded:
// User out of iCloud storage
notifyUserStorageFull()
case .notAuthenticated:
// User not signed into iCloud
promptiCloudSignIn()
case .limitExceeded:
// Too many records - split into batches of 400
splitAndRetry(records)
default:
log("CloudKit error: \(error.localizedDescription)")
}
}
```
## Conflict Resolution
```swift
func resolveConflict(local: CKRecord, server: CKRecord?) -> CKRecord {
guard let server = server else { return local }
// Strategy 1: Server wins (safest)
return server
// Strategy 2: Last writer wins (by modificationDate)
// return server.modificationDate! > local.modificationDate! ? server : local
// Strategy 3: Field-level merge
// let merged = CKRecord(recordType: local.recordType, recordID: local.recordID)
// merged["title"] = local["title"] // Keep local title
// merged["content"] = server["content"] // Keep server content
// return merged
// Strategy 4: Edit count (increment counter on each edit)
// let localCount = local["editCount"] as? Int ?? 0
// let serverCount = server["editCount"] as? Int ?? 0
// return localCount > serverCount ? local : server
}
```
## Record Zones
Private database supports custom zones with change tracking:
```swift
let zoneID = CKRecordZone.ID(zoneName: "MyAppZone", ownerName: CKCurrentUserDefaultName)
let zone = CKRecordZone(zoneID: zoneID)
// Create zone
try await database.save(zone)
// Create record in zone
let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: zoneID)
let record = CKRecord(recordType: "Note", recordID: recordID)
// Delete zone (deletes ALL records in it)
try await database.deleteRecordZone(withID: zoneID)
```
## Subscriptions & Push Notifications
```swift
// Subscribe to zone changes (private DB)
let subscription = CKRecordZoneSubscription(zoneID: zoneID)
let notificationInfo = CKSubscription.NotificationInfo()
notificationInfo.shouldSendContentAvailable = true // Silent push
subscription.notificationInfo = notificationInfo
try await database.save(subscription)
// Subscribe to query (public DB)
let predicate = NSPredicate(format: "category == %@", "important")
let querySubscription = CKQuerySubscription(
recordType: "Note",
predicate: predicate,
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
)
```
## Assets (Binary Data)
```swift
// Save image
let imageURL = FileManager.default.temporaryDirectory.appendingPathComponent("photo.jpg")
imageData.write(to: imageURL)
let asset = CKAsset(fileURL: imageURL)
record["photo"] = asset
try await database.save(record)
// Load image
if let asset = record["photo"] as? CKAsset, let url = asset.fileURL {
let data = try Data(contentsOf: url)
let image = UIImage(data: data)
}
```
## Sharing (CloudKit Sharing)
```swift
// Create share
let share = CKShare(rootRecord: record)
share.publicPermission = .readOnly
share[CKShare.SystemFieldKey.title] = "Shared Document"
// Save both
let operation = CKModifyRecordsOperation(recordsToSave: [record, share])
try await database.add(operation)
// Present sharing UI
let sharingController = UICloudSharingController(share: share, container: container)
present(sharingController, animated: true)
```
## Project Setup Checklist
1. **Apple Developer Program** membership required
2. **Xcode Signing & Capabilities**:
- Add iCloud capability
- Check CloudKit
- Create/select container (e.g., `iCloud.com.yourcompany.appname`)
- Background Modes → Remote notifications
3. **Container cannot be deleted** — name carefully
4. **Info.plist** (for background fetch):
```xml
UIBackgroundModes
remote-notification
```
## CloudKit Dashboard
Access at: https://icloud.developer.apple.com
- View/edit records, zones, subscriptions
- Monitor usage and quotas
- Deploy schema to production
- **Development vs Production** environments are separate
## Best Practices
1. **Always cache CKSyncEngine state token** — or sync breaks
2. **Batch operations to 400 records max** — avoid `limitExceeded`
3. **Store CKRecord metadata locally** — for conflict resolution
4. **Use encrypted fields for sensitive data** — `record.encryptedValues["key"]`
5. **Handle all error cases** — especially transient errors with retry
6. **Test on physical devices** — simulator has limitations
7. **Don't use enums in synced data** — use strings instead (forward compatibility)
8. **Keep change tokens after fetches** — commit only after local save succeeds
## References
### Implementation Guides
- [CKSyncEngine Guide](references/cksyncengine.md) — Complete implementation walkthrough (iOS 17+)
- [CRUD Operations](references/crud-operations.md) — Direct database operations, queries, assets
- [Operations Reference](references/operations.md) — All CKOperation classes, batching, pagination
- [Subscriptions & Notifications](references/subscriptions-notifications.md) — Push notifications, real-time sync
### Features
- [Sharing](references/sharing.md) — CKShare, UICloudSharingController, zone sharing
- [User Discovery](references/user-discovery.md) — CKUserIdentity, finding users, discoverability
- [Privacy & Security](references/privacy-security.md) — Encryption, access controls, GDPR compliance
### Reference
- [API Reference](references/api-reference.md) — Complete class/method listing for all CloudKit APIs
- [Error Handling](references/error-handling.md) — All CKError codes and retry strategies
- [Schema Design](references/schema-design.md) — Versioning, references, encryption, migrations
- [Troubleshooting](references/troubleshooting.md) — Common issues, debugging, CloudKit Dashboard
## External Resources
- [Apple Sample: CKSyncEngine](https://github.com/apple/sample-cloudkit-sync-engine)
- [Apple Sample: Zone Sharing](https://github.com/apple/sample-cloudkit-zonesharing)
- [Apple CloudKit Documentation](https://developer.apple.com/documentation/cloudkit)
- [CloudKit Dashboard](https://icloud.developer.apple.com)
- [WWDC Videos](https://developer.apple.com/videos/frameworks/cloudkit)