cloudkit

SKILL.md

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

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 for complete implementation guide.

Minimal Setup

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.

// 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 for complete error codes.

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

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:

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

// 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)

// 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)

// 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):
    <key>UIBackgroundModes</key>
    <array>
        <string>remote-notification</string>
    </array>
    

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 datarecord.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

Features

  • Sharing — CKShare, UICloudSharingController, zone sharing
  • User Discovery — CKUserIdentity, finding users, discoverability
  • Privacy & Security — Encryption, access controls, GDPR compliance

Reference

External Resources

Weekly Installs
9
First Seen
Feb 9, 2026
Installed on
codex9
gemini-cli9
opencode9
cursor8
github-copilot7
kimi-cli6