CloudKit Framework
Comprehensive guide to CloudKit for direct iCloud data storage, synchronization, subscriptions, and multi-user sharing.
Prerequisites
- •iOS 10+ (iOS 26 recommended)
- •Xcode 26+
- •iCloud capability with CloudKit enabled
- •Active Apple Developer account with admin permissions
CloudKit Architecture
CKContainer
The container is the top-level encapsulation of your app's CloudKit data:
import CloudKit // Default container (from entitlements) let container = CKContainer.default() // Custom container identifier let customContainer = CKContainer(identifier: "iCloud.com.yourcompany.yourapp") // Access container properties let containerID = container.containerIdentifier
CKDatabase Types
Each container has three databases with different visibility:
// Private database - user's own data (requires sign-in) let privateDB = container.privateCloudDatabase // Public database - visible to all users (read without sign-in) let publicDB = container.publicCloudDatabase // Shared database - data shared with this user by others let sharedDB = container.sharedCloudDatabase
Database Scope Summary:
| Database | Read | Write | Sign-in Required |
|---|---|---|---|
| Private | Owner only | Owner only | Yes |
| Public | Everyone | Signed-in users | Read: No, Write: Yes |
| Shared | Participants | Participants with permission | Yes |
CKRecordZone
Zones partition records within a database:
// Default zone (always exists)
let defaultZone = CKRecordZone.default()
// Custom zone for advanced sync features
let customZone = CKRecordZone(zoneName: "MyCustomZone")
// Create custom zone
privateDB.save(customZone) { zone, error in
if let error = error {
print("Zone creation failed: \(error)")
return
}
print("Zone created: \(zone?.zoneID.zoneName ?? "")")
}
When to use custom zones:
- •Enable atomic commits across multiple records
- •Track changes with change tokens for efficient sync
- •Share entire zones with other users
- •Support custom zone subscriptions
CKRecord Basics
Creating Records
// Create a record with a type name let noteRecord = CKRecord(recordType: "Note") noteRecord["title"] = "Meeting Notes" noteRecord["content"] = "Discuss Q4 goals" noteRecord["createdAt"] = Date() noteRecord["isPinned"] = false // Create with explicit ID let recordID = CKRecord.ID(recordName: "unique-note-123") let noteWithID = CKRecord(recordType: "Note", recordID: recordID) // Create in custom zone let zoneID = CKRecordZone.ID(zoneName: "NotesZone", ownerName: CKCurrentUserDefaultName) let zonedRecordID = CKRecord.ID(recordName: "note-456", zoneID: zoneID) let zonedNote = CKRecord(recordType: "Note", recordID: zonedRecordID)
Supported Field Types
let record = CKRecord(recordType: "MediaItem") // Basic types record["title"] = "Sunset Photo" as String record["viewCount"] = 42 as Int record["rating"] = 4.5 as Double record["isPublished"] = true as Bool // Date record["createdAt"] = Date() // Data (up to 1MB inline, use CKAsset for larger) record["thumbnail"] = Data() // Location record["location"] = CLLocation(latitude: 37.7749, longitude: -122.4194) // Asset (for large files) let fileURL = URL(fileURLWithPath: "/path/to/image.jpg") record["photo"] = CKAsset(fileURL: fileURL) // Reference to another record let albumID = CKRecord.ID(recordName: "album-123") record["album"] = CKRecord.Reference(recordID: albumID, action: .deleteSelf) // Arrays record["tags"] = ["nature", "sunset", "photography"] as [String]
Reference Actions
// Delete this record if parent is deleted CKRecord.Reference(recordID: parentID, action: .deleteSelf) // No automatic action CKRecord.Reference(recordID: parentID, action: .none)
Saving Records
// Simple save
privateDB.save(noteRecord) { savedRecord, error in
if let error = error {
print("Save failed: \(error)")
return
}
print("Saved record: \(savedRecord?.recordID.recordName ?? "")")
}
// Async/await (iOS 15+)
func saveNote(_ record: CKRecord) async throws -> CKRecord {
return try await privateDB.save(record)
}
Fetching by ID
let recordID = CKRecord.ID(recordName: "note-123")
// Callback-based
privateDB.fetch(withRecordID: recordID) { record, error in
if let error = error {
print("Fetch failed: \(error)")
return
}
if let note = record {
print("Title: \(note["title"] ?? "")")
}
}
// Async/await
func fetchNote(id: String) async throws -> CKRecord {
let recordID = CKRecord.ID(recordName: id)
return try await privateDB.record(for: recordID)
}
Deleting Records
privateDB.delete(withRecordID: recordID) { deletedID, error in
if let error = error {
print("Delete failed: \(error)")
return
}
print("Deleted: \(deletedID?.recordName ?? "")")
}
CKQuery and Fetching
Basic Query with Predicate
// Fetch all notes let allNotesPredicate = NSPredicate(value: true) let allNotesQuery = CKQuery(recordType: "Note", predicate: allNotesPredicate) // Fetch pinned notes let pinnedPredicate = NSPredicate(format: "isPinned == %@", NSNumber(value: true)) let pinnedQuery = CKQuery(recordType: "Note", predicate: pinnedPredicate) // String contains (case-insensitive) let searchPredicate = NSPredicate(format: "title CONTAINS[cd] %@", "meeting") let searchQuery = CKQuery(recordType: "Note", predicate: searchPredicate) // Date comparison let recentPredicate = NSPredicate(format: "createdAt > %@", Date().addingTimeInterval(-86400) as NSDate) let recentQuery = CKQuery(recordType: "Note", predicate: recentPredicate) // Reference match let albumRef = CKRecord.Reference(recordID: albumID, action: .none) let albumPredicate = NSPredicate(format: "album == %@", albumRef)
Sorting Results
let query = CKQuery(recordType: "Note", predicate: NSPredicate(value: true))
// Single sort
query.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
// Multiple sorts
query.sortDescriptors = [
NSSortDescriptor(key: "isPinned", ascending: false),
NSSortDescriptor(key: "createdAt", ascending: false)
]
CKQueryOperation for Pagination
func fetchNotes(cursor: CKQueryOperation.Cursor? = nil) {
let operation: CKQueryOperation
if let cursor = cursor {
operation = CKQueryOperation(cursor: cursor)
} else {
let query = CKQuery(recordType: "Note", predicate: NSPredicate(value: true))
query.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
operation = CKQueryOperation(query: query)
}
operation.resultsLimit = 50
var fetchedRecords: [CKRecord] = []
operation.recordMatchedBlock = { recordID, result in
switch result {
case .success(let record):
fetchedRecords.append(record)
case .failure(let error):
print("Record fetch error: \(error)")
}
}
operation.queryResultBlock = { result in
switch result {
case .success(let cursor):
print("Fetched \(fetchedRecords.count) records")
if let cursor = cursor {
// More results available - fetch next page
self.fetchNotes(cursor: cursor)
}
case .failure(let error):
print("Query failed: \(error)")
}
}
privateDB.add(operation)
}
Fetching Specific Fields Only
let operation = CKQueryOperation(query: query) operation.desiredKeys = ["title", "createdAt"] // Only fetch these fields
Batch Operations
CKModifyRecordsOperation
func saveMultipleRecords(_ records: [CKRecord]) {
let operation = CKModifyRecordsOperation(
recordsToSave: records,
recordIDsToDelete: nil
)
// Per-record progress
operation.perRecordProgressBlock = { record, progress in
print("Upload progress for \(record.recordID.recordName): \(progress)")
}
// Per-record result
operation.perRecordSaveBlock = { recordID, result in
switch result {
case .success(let record):
print("Saved: \(record.recordID.recordName)")
case .failure(let error):
print("Failed to save \(recordID.recordName): \(error)")
}
}
// Overall completion
operation.modifyRecordsResultBlock = { result in
switch result {
case .success:
print("Batch save completed")
case .failure(let error):
print("Batch operation failed: \(error)")
}
}
// Save policy
operation.savePolicy = .changedKeys // Only upload changed fields
// Atomic commit (requires custom zone)
operation.isAtomic = true
privateDB.add(operation)
}
Save Policies
// Only save fields that changed since last fetch operation.savePolicy = .changedKeys // Save all fields (overwrites server values) operation.savePolicy = .allKeys // Fail if record has changed on server operation.savePolicy = .ifServerRecordUnchanged
Batch Delete
func deleteRecords(_ recordIDs: [CKRecord.ID]) {
let operation = CKModifyRecordsOperation(
recordsToSave: nil,
recordIDsToDelete: recordIDs
)
operation.perRecordDeleteBlock = { recordID, result in
switch result {
case .success:
print("Deleted: \(recordID.recordName)")
case .failure(let error):
print("Delete failed: \(error)")
}
}
privateDB.add(operation)
}
Subscriptions and Notifications
Query Subscription
Subscribe to changes matching a predicate:
func subscribeToNewNotes() {
let predicate = NSPredicate(format: "isPinned == %@", NSNumber(value: true))
let subscription = CKQuerySubscription(
recordType: "Note",
predicate: predicate,
subscriptionID: "pinned-notes-subscription",
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true // Silent push
notification.alertBody = "A pinned note was updated"
notification.soundName = "default"
subscription.notificationInfo = notification
privateDB.save(subscription) { subscription, error in
if let error = error {
print("Subscription failed: \(error)")
return
}
print("Subscribed to pinned notes")
}
}
Database Subscription
Subscribe to all changes in a database:
func subscribeToDatabaseChanges() {
let subscription = CKDatabaseSubscription(subscriptionID: "private-db-changes")
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
privateDB.save(subscription) { subscription, error in
if let error = error {
print("Database subscription failed: \(error)")
return
}
print("Subscribed to all private database changes")
}
}
Record Zone Subscription
Subscribe to changes in a specific zone:
func subscribeToZoneChanges(zoneID: CKRecordZone.ID) {
let subscription = CKRecordZoneSubscription(
zoneID: zoneID,
subscriptionID: "zone-\(zoneID.zoneName)-changes"
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
privateDB.save(subscription) { subscription, error in
if let error = error {
print("Zone subscription failed: \(error)")
return
}
print("Subscribed to zone: \(zoneID.zoneName)")
}
}
Processing Push Notifications
In your App Delegate:
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)
switch notification?.notificationType {
case .query:
if let queryNotification = notification as? CKQueryNotification {
let recordID = queryNotification.recordID
print("Record changed: \(recordID?.recordName ?? "")")
}
case .database:
print("Database changed - fetch changes")
fetchDatabaseChanges()
case .recordZone:
if let zoneNotification = notification as? CKRecordZoneNotification {
print("Zone changed: \(zoneNotification.recordZoneID?.zoneName ?? "")")
}
default:
break
}
completionHandler(.newData)
}
Record Zones and Change Tokens
Creating a Custom Zone
func createNotesZone() async throws {
let zoneID = CKRecordZone.ID(zoneName: "NotesZone", ownerName: CKCurrentUserDefaultName)
let zone = CKRecordZone(zoneID: zoneID)
_ = try await privateDB.save(zone)
print("Notes zone created")
}
Fetching Zone Changes with Tokens
class CloudKitSync {
var zoneChangeToken: CKServerChangeToken?
func fetchZoneChanges(zoneID: CKRecordZone.ID) {
let configuration = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
configuration.previousServerChangeToken = zoneChangeToken
let operation = CKFetchRecordZoneChangesOperation(
recordZoneIDs: [zoneID],
configurationsByRecordZoneID: [zoneID: configuration]
)
operation.recordWasChangedBlock = { recordID, result in
switch result {
case .success(let record):
print("Changed: \(record.recordType) - \(recordID.recordName)")
// Update local cache
case .failure(let error):
print("Record change error: \(error)")
}
}
operation.recordWithIDWasDeletedBlock = { recordID, recordType in
print("Deleted: \(recordType) - \(recordID.recordName)")
// Remove from local cache
}
operation.recordZoneChangeTokensUpdatedBlock = { zoneID, token, _ in
// Save token for next fetch
self.zoneChangeToken = token
}
operation.recordZoneFetchResultBlock = { zoneID, result in
switch result {
case .success((let token, _)):
self.zoneChangeToken = token
print("Zone sync complete")
case .failure(let error):
print("Zone fetch failed: \(error)")
}
}
privateDB.add(operation)
}
}
Sharing with CKShare
Record Hierarchy Sharing
Share a specific record and its children:
func shareAlbum(_ album: CKRecord) async throws -> CKShare {
// Create share for the root record
let share = CKShare(rootRecord: album)
// Configure share metadata
share[CKShare.SystemFieldKey.title] = album["name"]
if let coverData = album["coverImage"] as? Data {
share[CKShare.SystemFieldKey.thumbnailImageData] = coverData
}
share[CKShare.SystemFieldKey.shareType] = "com.yourapp.album"
// Set permissions
share.publicPermission = .none // Private share (invite only)
// Save share and root record together
let operation = CKModifyRecordsOperation(
recordsToSave: [album, share],
recordIDsToDelete: nil
)
return try await withCheckedThrowingContinuation { continuation in
operation.modifyRecordsResultBlock = { result in
switch result {
case .success:
continuation.resume(returning: share)
case .failure(let error):
continuation.resume(throwing: error)
}
}
privateDB.add(operation)
}
}
Record Zone Sharing
Share an entire zone:
func shareZone(_ zoneID: CKRecordZone.ID) async throws -> CKShare {
let share = CKShare(recordZoneID: zoneID)
share[CKShare.SystemFieldKey.title] = "Shared Notes"
share.publicPermission = .none
let operation = CKModifyRecordsOperation(
recordsToSave: [share],
recordIDsToDelete: nil
)
return try await withCheckedThrowingContinuation { continuation in
operation.modifyRecordsResultBlock = { result in
switch result {
case .success:
continuation.resume(returning: share)
case .failure(let error):
continuation.resume(throwing: error)
}
}
privateDB.add(operation)
}
}
Detecting Zone-Wide Share
func isZoneWideShare(_ metadata: CKShare.Metadata) -> Bool {
return metadata.share.recordID.recordName == CKRecordNameZoneWideShare
}
Share Permissions
// Public permission (anyone with link) share.publicPermission = .none // Invite only share.publicPermission = .readOnly // Anyone can read share.publicPermission = .readWrite // Anyone can read/write // Participant permissions let participant = share.participants.first participant?.permission = .readOnly participant?.permission = .readWrite
Adding Participants Programmatically
func addParticipant(email: String, to share: CKShare) async throws {
let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: email)
let fetchOp = CKFetchShareParticipantsOperation(userIdentityLookupInfos: [lookupInfo])
var participant: CKShare.Participant?
fetchOp.perShareParticipantResultBlock = { lookupInfo, result in
switch result {
case .success(let p):
participant = p
participant?.permission = .readWrite
case .failure(let error):
print("Failed to fetch participant: \(error)")
}
}
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
fetchOp.fetchShareParticipantsResultBlock = { result in
switch result {
case .success:
if let participant = participant {
share.addParticipant(participant)
}
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
CKContainer.default().add(fetchOp)
}
}
UICloudSharingController
Info.plist Requirement
Add to your Info.plist:
<key>CKSharingSupported</key> <true/>
Presenting for New Share
class SharingViewController: UIViewController, UICloudSharingControllerDelegate {
var recordToShare: CKRecord?
@IBAction func shareButtonTapped(_ sender: UIBarButtonItem) {
guard let record = recordToShare else { return }
let sharingController = UICloudSharingController { controller, completion in
self.createShare(for: record, completion: completion)
}
sharingController.delegate = self
// REQUIRED for iPad - set popover source
if let popover = sharingController.popoverPresentationController {
popover.barButtonItem = sender
}
present(sharingController, animated: true)
}
private func createShare(for record: CKRecord,
completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) {
let share = CKShare(rootRecord: record)
share[CKShare.SystemFieldKey.title] = record["title"]
let operation = CKModifyRecordsOperation(
recordsToSave: [record, share],
recordIDsToDelete: nil
)
operation.modifyRecordsResultBlock = { result in
DispatchQueue.main.async {
switch result {
case .success:
completion(share, CKContainer.default(), nil)
case .failure(let error):
completion(nil, nil, error)
}
}
}
CKContainer.default().privateCloudDatabase.add(operation)
}
// MARK: - UICloudSharingControllerDelegate
func cloudSharingController(_ csc: UICloudSharingController,
failedToSaveShareWithError error: Error) {
print("Failed to save share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return recordToShare?["title"] as? String
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return recordToShare?["thumbnailData"] as? Data
}
}
Managing Existing Share
func manageShare(_ share: CKShare) {
let sharingController = UICloudSharingController(
share: share,
container: CKContainer.default()
)
sharingController.delegate = self
// Always set popover source for iPad
if let popover = sharingController.popoverPresentationController {
popover.sourceView = view
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
}
present(sharingController, animated: true)
}
Configuring Available Permissions
let sharingController = UICloudSharingController(share: share, container: container)
// Allow only specific options
sharingController.availablePermissions = [
.allowPublic, // Allow "Anyone with link" option
.allowPrivate, // Allow "Only invited people" option
.allowReadOnly, // Allow read-only permission
.allowReadWrite // Allow read/write permission
]
Share Operations
Fetching Share Metadata
func fetchShareMetadata(from url: URL) async throws -> CKShare.Metadata {
let operation = CKFetchShareMetadataOperation(shareURLs: [url])
return try await withCheckedThrowingContinuation { continuation in
var fetchedMetadata: CKShare.Metadata?
operation.perShareMetadataResultBlock = { url, result in
switch result {
case .success(let metadata):
fetchedMetadata = metadata
case .failure(let error):
continuation.resume(throwing: error)
}
}
operation.fetchShareMetadataResultBlock = { result in
switch result {
case .success:
if let metadata = fetchedMetadata {
continuation.resume(returning: metadata)
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
CKContainer.default().add(operation)
}
}
Accepting a Share
func acceptShare(_ metadata: CKShare.Metadata) async throws {
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
operation.perShareResultBlock = { metadata, result in
switch result {
case .success(let share):
print("Accepted share: \(share.recordID.recordName)")
case .failure(let error):
print("Failed to accept: \(error)")
}
}
operation.acceptSharesResultBlock = { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
CKContainer.default().add(operation)
}
}
Processing Share in App Delegate
// Scene-based apps (iOS 13+)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func windowScene(_ windowScene: UIWindowScene,
userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
Task {
do {
try await acceptShare(cloudKitShareMetadata)
// Navigate to shared content
} catch {
print("Failed to accept share: \(error)")
}
}
}
}
// Non-scene apps
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
Task {
try? await acceptShare(cloudKitShareMetadata)
}
}
}
SwiftData + CloudKit Integration
Basic SwiftData with CloudKit
SwiftData can use CloudKit automatically:
let config = ModelConfiguration(cloudKitDatabase: .automatic) let container = try ModelContainer(for: Note.self, configurations: config)
Initializing CloudKit Schema for SwiftData
Before releasing to production, initialize the CloudKit schema:
@main
struct MyApp: App {
let container: ModelContainer
init() {
let config = ModelConfiguration()
do {
#if DEBUG
try autoreleasepool {
let desc = NSPersistentStoreDescription(url: config.url)
let opts = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.yourcompany.yourapp"
)
desc.cloudKitContainerOptions = opts
desc.shouldAddStoreAsynchronously = false
if let mom = NSManagedObjectModel.makeManagedObjectModel(
for: [Note.self, Tag.self]
) {
let container = NSPersistentCloudKitContainer(
name: "MyApp",
managedObjectModel: mom
)
container.persistentStoreDescriptions = [desc]
container.loadPersistentStores { _, error in
if let error = error {
fatalError(error.localizedDescription)
}
}
try container.initializeCloudKitSchema()
if let store = container.persistentStoreCoordinator.persistentStores.first {
try container.persistentStoreCoordinator.remove(store)
}
}
}
#endif
container = try ModelContainer(for: Note.self, Tag.self, configurations: config)
} catch {
fatalError("Failed to configure SwiftData: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Specifying CloudKit Container
// Use specific CloudKit container
let config = ModelConfiguration(
cloudKitDatabase: .private("iCloud.com.yourcompany.yourapp")
)
// Disable CloudKit sync entirely
let localConfig = ModelConfiguration(cloudKitDatabase: .none)
Schema Compatibility Requirements
For SwiftData models to sync via CloudKit:
@Model
class Note {
// All relationships must be optional
var folder: Folder?
var tags: [Tag]?
// No unique constraints
// @Attribute(.unique) var id: String // NOT SUPPORTED
// No deny delete rules
// @Relationship(deleteRule: .deny) // NOT SUPPORTED
var title: String = ""
var content: String = ""
init(title: String = "") {
self.title = title
}
}
Error Handling
Common CloudKit Errors
func handleCloudKitError(_ error: Error) {
guard let ckError = error as? CKError else {
print("Unknown error: \(error)")
return
}
switch ckError.code {
case .networkFailure, .networkUnavailable:
print("Network issue - retry later")
case .notAuthenticated:
print("User not signed into iCloud")
case .quotaExceeded:
print("iCloud storage full")
case .serverRecordChanged:
// Handle conflict
if let serverRecord = ckError.userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord {
print("Server has newer version")
// Merge or overwrite
}
case .zoneNotFound:
print("Zone doesn't exist - create it")
case .userDeletedZone:
print("User deleted the zone - recreate")
case .participantMayNeedVerification:
print("Participant needs to verify their account")
case .alreadyShared:
print("Record is already part of another share")
case .partialFailure:
// Check individual record errors
if let partialErrors = ckError.userInfo[CKPartialErrorsByItemIDKey] as? [CKRecord.ID: Error] {
for (recordID, recordError) in partialErrors {
print("Error for \(recordID.recordName): \(recordError)")
}
}
default:
print("CloudKit error: \(ckError.localizedDescription)")
}
}
Retry Logic
func retryableOperation<T>(maxRetries: Int = 3,
operation: @escaping () async throws -> T) async throws -> T {
var lastError: Error?
for attempt in 1...maxRetries {
do {
return try await operation()
} catch let error as CKError {
lastError = error
if let retryAfter = error.retryAfterSeconds {
try await Task.sleep(nanoseconds: UInt64(retryAfter * 1_000_000_000))
} else if error.code == .networkFailure || error.code == .serviceUnavailable {
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt)) * 1_000_000_000))
} else {
throw error // Non-retryable error
}
}
}
throw lastError ?? CKError(.internalError)
}
Best Practices
1. Use Custom Zones for Private Data
// Custom zones enable: // - Change tracking with tokens // - Atomic transactions // - Zone-based sharing let zoneID = CKRecordZone.ID(zoneName: "UserData", ownerName: CKCurrentUserDefaultName)
2. Handle Errors Per-Record in Batch Operations
operation.perRecordSaveBlock = { recordID, result in
switch result {
case .success(let record):
// Update local cache
case .failure(let error):
// Handle individual failure
}
}
3. Use Change Tokens for Efficient Sync
// Store tokens persistently UserDefaults.standard.set(tokenData, forKey: "zoneChangeToken") // Use tokens to fetch only changes since last sync configuration.previousServerChangeToken = storedToken
4. Always Set Popover Source on iPad
// REQUIRED - crashes on iPad without this
if let popover = sharingController.popoverPresentationController {
popover.barButtonItem = sender
// OR
popover.sourceView = view
popover.sourceRect = rect
}
5. Test with Multiple iCloud Accounts
- •Test sharing between different accounts
- •Test on devices signed out of iCloud
- •Test with network disabled
- •Test quota exceeded scenarios
6. Design for Eventual Consistency
// CloudKit syncs asynchronously - don't assume immediate consistency // Use subscriptions for real-time updates // Display sync status to users