--- name: axiom-cloudkit-ref description: Use when implementing 'CloudKit sync', 'CKSyncEngine', 'CKRecord', 'CKDatabase', 'SwiftData CloudKit', 'shared database', 'public database', 'CloudKit zones', 'conflict resolution' - comprehensive CloudKit database APIs and modern sync patterns reference license: MIT compatibility: iOS 10.0+, iPadOS 13.0+, macOS 10.12+ metadata: version: "1.0.0" last-updated: "2025-12-12" --- # CloudKit Reference **Purpose**: Comprehensive CloudKit reference for database-based iCloud storage and sync **Availability**: iOS 10.0+ (basic), iOS 17.0+ (CKSyncEngine), iOS 17.0+ (SwiftData integration) **Context**: Modern CloudKit sync via CKSyncEngine (WWDC 2023) or SwiftData integration ## When to Use This Skill Use this skill when: - Implementing structured data sync to iCloud - Choosing between SwiftData+CloudKit, CKSyncEngine, or raw CloudKit APIs - Setting up public/private/shared databases - Implementing conflict resolution - Debugging CloudKit sync issues - Monitoring CloudKit performance **NOT for**: Simple file sync (use `axiom-icloud-drive-ref` instead) ## Overview **CloudKit is for STRUCTURED DATA sync** (records with relationships), not simple file sync. Three modern approaches: 1. **SwiftData + CloudKit** (Easiest, iOS 17+) 2. **CKSyncEngine** (Custom persistence, iOS 17+, WWDC 2023) 3. **Raw CloudKit APIs** (Maximum control, more complexity) --- ## Approach 1: SwiftData + CloudKit (Recommended) **When to use**: iOS 17+ apps with SwiftData models **Limitations**: - Private database only (no public/shared) - Automatic sync (less control) - SwiftData constraints apply ```swift // ✅ CORRECT: SwiftData with CloudKit sync import SwiftData @Model class Task { var title: String var isCompleted: Bool var dueDate: Date init(title: String, isCompleted: Bool = false, dueDate: Date) { self.title = title self.isCompleted = isCompleted self.dueDate = dueDate } } // Configure CloudKit container let container = try ModelContainer( for: Task.self, configurations: ModelConfiguration( cloudKitDatabase: .private("iCloud.com.example.app") ) ) // That's it! Sync happens automatically ``` **Entitlements required**: - iCloud capability - CloudKit container **Use `axiom-swiftdata` skill for SwiftData details** --- ## Approach 2: CKSyncEngine (Modern, WWDC 2023) **When to use**: Custom persistence (SQLite, GRDB, JSON) with cloud sync **Advantages over raw CloudKit**: - Manages fetch/upload cycles automatically - Handles conflicts - Manages account changes - Recommended over manual CKDatabase operations ```swift // ✅ CORRECT: CKSyncEngine setup import CloudKit class SyncManager { let syncEngine: CKSyncEngine init() throws { let config = CKSyncEngine.Configuration( database: CKContainer.default().privateCloudDatabase, stateSerialization: loadSyncState(), delegate: self ) syncEngine = try CKSyncEngine(config) } // Implement delegate methods } extension SyncManager: CKSyncEngineDelegate { // Handle events func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async { switch event { case .stateUpdate(let stateUpdate): saveSyncState(stateUpdate.stateSerialization) case .accountChange(let change): handleAccountChange(change) case .fetchedDatabaseChanges(let changes): applyDatabaseChanges(changes) case .fetchedRecordZoneChanges(let changes): applyRecordChanges(changes) case .sentRecordZoneChanges(let changes): handleSentChanges(changes) case .willFetchChanges, .didFetchChanges, .willSendChanges, .didSendChanges: // Optional lifecycle events break @unknown default: break } } // Next batch of changes to send func nextRecordZoneChangeBatch( _ context: CKSyncEngine.SendChangesContext, syncEngine: CKSyncEngine ) async -> CKSyncEngine.RecordZoneChangeBatch? { // Return pending local changes let pendingChanges = getPendingLocalChanges() return CKSyncEngine.RecordZoneChangeBatch( pendingSaves: pendingChanges, recordIDsToDelete: [] ) } } ``` **Key concepts**: - **State serialization**: Persist sync state between app launches - **Events**: Delegate receives events for changes - **Batches**: You provide pending changes, engine uploads them - **Automatic conflict resolution**: Engine handles basic conflicts --- ## Approach 3: Raw CloudKit APIs (Legacy) **When to use**: Only if CKSyncEngine doesn't fit (rare) **Core types**: - `CKContainer` — Entry point - `CKDatabase` — Public/private/shared scope - `CKRecord` — Individual data record - `CKRecordZone` — Logical grouping - `CKAsset` — Binary file storage ### Basic Operations ```swift // ✅ Container and database let container = CKContainer.default() let privateDatabase = container.privateCloudDatabase let publicDatabase = container.publicCloudDatabase // ✅ Create record let record = CKRecord(recordType: "Task") record["title"] = "Buy groceries" record["isCompleted"] = false record["dueDate"] = Date() // ✅ Save record try await privateDatabase.save(record) // ✅ Fetch record let recordID = CKRecord.ID(recordName: "task-123") let fetchedRecord = try await privateDatabase.record(for: recordID) // ✅ Query records let predicate = NSPredicate(format: "isCompleted == NO") let query = CKQuery(recordType: "Task", predicate: predicate) let (matchResults, _) = try await privateDatabase.records(matching: query) for result in matchResults { if case .success(let record) = result.1 { print("Task: \(record["title"] as? String ?? "")") } } // ✅ Delete record try await privateDatabase.deleteRecord(withID: recordID) ``` ### Update Record ```swift // ✅ Fetch-then-modify-then-save (prevents serverRecordChanged errors) let record = try await privateDatabase.record(for: recordID) record["title"] = "Updated title" record["isCompleted"] = true try await privateDatabase.save(record) // ✅ Batch modify (save + delete in one operation) let operation = CKModifyRecordsOperation( recordsToSave: [updatedRecord1, updatedRecord2], recordIDsToDelete: [deletedID] ) operation.perRecordSaveBlock = { recordID, result in switch result { case .success: print("Saved: \(recordID)") case .failure(let error): print("Failed: \(recordID) — \(error)") } } try await privateDatabase.add(operation) ``` ### Conflict Resolution ```swift // ✅ Handle conflicts with savePolicy let operation = CKModifyRecordsOperation( recordsToSave: [record], recordIDsToDelete: nil ) // Save only if server version unchanged operation.savePolicy = .ifServerRecordUnchanged // OR: Always overwrite server operation.savePolicy = .changedKeys // Only changed fields operation.modifyRecordsResultBlock = { result in switch result { case .success: print("Saved") case .failure(let error as CKError): if error.code == .serverRecordChanged { // Conflict - merge manually let serverRecord = error.serverRecord let clientRecord = error.clientRecord let merged = mergeRecords(server: serverRecord, client: clientRecord) // Retry with merged record } } } privateDatabase.add(operation) ``` --- ## Database Scopes | Scope | Accessibility | SwiftData Support | Use Case | |-------|---------------|-------------------|----------| | **Private** | User only | ✅ Yes | Personal user data | | **Public** | All users | ❌ No | Shared/public content | | **Shared** | Invited users | ❌ No | Collaboration | ### Private Database ```swift // ✅ Private database (most common) let privateDB = CKContainer.default().privateCloudDatabase // User must be signed into iCloud // Data syncs across user's devices // Not visible to other users ``` ### Public Database ```swift // ✅ Public database (for shared content) let publicDB = CKContainer.default().publicCloudDatabase // Accessible to all app users // Even unauthenticated users can read // Writes require authentication // Use for: Leaderboards, public content, discovery ``` ### Shared Database ```swift // ✅ Shared database (collaboration) let sharedDB = CKContainer.default().sharedCloudDatabase // For CKShare-based collaboration // Users invited to specific record zones // Use for: Shared documents, team data ``` --- ## CloudKit Assets (Files) ```swift // ✅ Store files as CKAsset let imageURL = saveImageToTempFile(image) // Must be file URL let asset = CKAsset(fileURL: imageURL) let record = CKRecord(recordType: "Photo") record["image"] = asset record["caption"] = "Sunset" try await privateDatabase.save(record) // ✅ Retrieve asset let fetchedRecord = try await privateDatabase.record(for: recordID) if let asset = fetchedRecord["image"] as? CKAsset, let fileURL = asset.fileURL { let imageData = try Data(contentsOf: fileURL) let image = UIImage(data: imageData) } ``` **Important**: CKAsset requires a **file URL**, not Data. Write data to temp file first. --- ## CloudKit Console (Monitoring - WWDC 2024) ### Developer Notifications Set up alerts for: - Schema changes - Quota exceeded - High error rates - Custom thresholds ### Telemetry Monitor: - Request count - Error rate - Latency (p50, p95, p99) - Bandwidth usage ### Logs View: - Individual requests - Error details - Performance bottlenecks **Access**: https://icloud.developer.apple.com/dashboard --- ## Common Patterns ### Pattern 1: Initial Sync ```swift // ✅ Fetch all records on first launch func performInitialSync() async throws { let predicate = NSPredicate(value: true) // All records let query = CKQuery(recordType: "Task", predicate: predicate) let (results, _) = try await privateDatabase.records(matching: query) for result in results { if case .success(let record) = result.1 { saveToLocalDatabase(record) } } } ``` ### Pattern 2: Incremental Sync ```swift // ✅ Use CKServerChangeToken for incremental fetches func fetchChanges(since token: CKServerChangeToken?) async throws { let zoneID = CKRecordZone.ID(zoneName: "Tasks") let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration( previousServerChangeToken: token ) let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [zoneID], configurationsByRecordZoneID: [zoneID: config] ) operation.recordWasChangedBlock = { recordID, result in if case .success(let record) = result { updateLocalDatabase(with: record) } } operation.recordWithIDWasDeletedBlock = { recordID, _ in deleteFromLocalDatabase(recordID) } operation.recordZoneFetchResultBlock = { zoneID, result in if case .success(let (token, _, _)) = result { saveChangeToken(token) // For next fetch } } try await privateDatabase.add(operation) } ``` --- ## Entitlements Required entitlements in Xcode: ```xml com.apple.developer.icloud-services CloudKit com.apple.developer.icloud-container-identifiers iCloud.com.example.app ``` **Setup**: 1. Xcode → Target → Signing & Capabilities 2. "+ Capability" → iCloud 3. Check "CloudKit" 4. Select or create container --- ## Subscriptions (Push Notifications) ### Database Subscription ```swift // ✅ Get notified of ANY change in private database let subscription = CKDatabaseSubscription(subscriptionID: "all-changes") let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true // Silent push subscription.notificationInfo = notificationInfo try await privateDatabase.save(subscription) ``` ### Query Subscription ```swift // ✅ Get notified when records matching a query change let predicate = NSPredicate(format: "priority > 3") let subscription = CKQuerySubscription( recordType: "Task", predicate: predicate, subscriptionID: "high-priority-tasks", options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] ) let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.alertBody = "High priority task changed" notificationInfo.shouldBadge = true subscription.notificationInfo = notificationInfo try await privateDatabase.save(subscription) ``` ### Zone Subscription ```swift // ✅ Get notified of any change in a specific zone let zoneID = CKRecordZone.ID(zoneName: "Tasks") let subscription = CKRecordZoneSubscription( zoneID: zoneID, subscriptionID: "tasks-zone" ) let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true subscription.notificationInfo = notificationInfo try await privateDatabase.save(subscription) ``` ### Handling Push Notifications ```swift // In AppDelegate func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult { let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) if notification.subscriptionID == "all-changes" { try? await fetchChanges(since: savedChangeToken) return .newData } return .noData } ``` --- ## Sharing Records ### Create a Share ```swift // ✅ Share a record with other users let record = try await privateDatabase.record(for: recordID) // Record must be in a custom zone (not 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) ``` ### Present Sharing UI ```swift import CloudKit import UIKit // ✅ UIKit sharing controller let sharingController = UICloudSharingController(share: share, container: container) sharingController.delegate = self present(sharingController, animated: true) // Delegate methods extension ViewController: UICloudSharingControllerDelegate { func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) { print("Share failed: \(error)") } func itemTitle(for csc: UICloudSharingController) -> String? { return "My Shared List" } } ``` ### Manage Participants ```swift // ✅ Check participants for participant in share.participants { print("\(participant.userIdentity.nameComponents?.givenName ?? "Unknown")") print(" Acceptance: \(participant.acceptanceStatus)") print(" Permission: \(participant.permission)") // .readOnly, .readWrite, .none } // ✅ Remove participant share.removeParticipant(participant) try await privateDatabase.save(share) ``` ### Accept a Share ```swift // In SceneDelegate or AppDelegate func userDidAcceptCloudKitShareWith(_ cloudKitShareMetadata: CKShare.Metadata) { let operation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata]) operation.acceptSharesResultBlock = { result in switch result { case .success: print("Share accepted") case .failure(let error): print("Accept failed: \(error)") } } CKContainer(identifier: cloudKitShareMetadata.containerIdentifier) .add(operation) } ``` --- ## Quick Reference | Task | Modern API (iOS 17+) | Legacy API | |------|----------------------|------------| | Structured data sync | SwiftData + CloudKit | CKSyncEngine or CKDatabase | | Custom persistence sync | CKSyncEngine | CKDatabase | | Conflict resolution | Automatic (SwiftData/CKSyncEngine) | Manual (savePolicy) | | Account changes | Handled automatically | Manual detection | | Monitoring | CloudKit Console telemetry | Manual logging | --- ## Related Skills - `axiom-swiftdata` — SwiftData implementation details - `axiom-storage` — Choose CloudKit vs iCloud Drive - `axiom-icloud-drive-ref` — File-based iCloud sync - `axiom-cloud-sync-diag` — Debug CloudKit sync issues --- **Last Updated**: 2025-12-12 **Skill Type**: Reference **Minimum iOS**: 10.0 (basic), 17.0 (CKSyncEngine, SwiftData integration) **WWDC Sessions**: 2023-10188 (CKSyncEngine), 2024-10122 (CloudKit Console)