Skip to content
Back to skills

Ax Cloud Storage

ASecurity

CloudKit sync, CKSyncEngine, iCloud Drive, NSFileCoordinator, conflict resolution, CKRecord, CKShare, subscriptions, NSUbiquitousKeyValueStore, offline-first patterns

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
developmentswiftsqltestinggitapidatabaseperformance

Works with

  • cursor
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add Kasempiternal/axiom-v2 --skill ax-cloud-storage --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ax Cloud Storage?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ax Cloud Storage
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kasempiternal-ax-cloud-storage/badge)](https://www.skillsdirectory.com/skills/kasempiternal-ax-cloud-storage)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ax-cloud-storage
description: CloudKit sync, CKSyncEngine, iCloud Drive, NSFileCoordinator, conflict resolution, CKRecord, CKShare, subscriptions, NSUbiquitousKeyValueStore, offline-first patterns
license: MIT
metadata:
  version: "2.0.0"
  last-updated: "2026-02-23"
  sources: ["axiom-cloud-sync", "axiom-cloud-sync-diag", "axiom-cloudkit-ref", "axiom-icloud-drive-ref"]
---

# Cloud Storage

## Quick Patterns

### CloudKit vs iCloud Drive Decision

```
What are you syncing?
├─ Structured data (records, fields) → CloudKit
│   ├─ Using SwiftData? → SwiftData + CloudKit container
│   ├─ Using SQLiteData? → CKSyncEngine (manual sync)
│   └─ Custom data model? → CKSyncEngine or raw CloudKit APIs
│
├─ User documents/files → iCloud Drive
│   ├─ Standard document types? → UIDocument + ubiquitous container
│   ├─ Custom file formats? → NSFileCoordinator
│   └─ Small preferences (<1MB)? → NSUbiquitousKeyValueStore
│
└─ Both? → CloudKit for metadata, iCloud Drive for large files
```

### Offline-First Pattern (Mandatory)

```swift
// ALWAYS: Write local first, sync in background
func saveTrack(_ track: Track) async throws {
    // 1. Save locally FIRST (instant, works offline)
    try database.insert(track)

    // 2. Queue for sync (background, can fail)
    syncEngine.queueChange(.save(track))

    // 3. Never block UI on sync
    // Sync happens asynchronously
}

// NEVER: Require network for local operations
// WRONG:
func saveTrack(_ track: Track) async throws {
    try await cloudKit.save(track)  // Fails offline!
}
```

### CloudKit Three Approaches

```swift
// Approach 1: SwiftData + CloudKit (simplest)
let config = ModelConfiguration(
    cloudKitDatabase: .private("iCloud.com.myapp")
)
let container = try ModelContainer(for: Track.self, configurations: config)
// Automatic sync - no additional code needed

// Approach 2: CKSyncEngine (WWDC 2023 - recommended for custom sync)
let syncEngine = CKSyncEngine(
    configuration: CKSyncEngine.Configuration(
        database: CKContainer.default().privateCloudDatabase,
        stateSerialization: loadSavedState(),
        delegate: self
    )
)

// Approach 3: Raw CloudKit APIs (full control)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
```

### CKSyncEngine Delegate

```swift
class SyncDelegate: CKSyncEngineDelegate {
    func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) {
        switch event {
        case .stateUpdate(let stateUpdate):
            // Save state for persistence across launches
            saveSyncState(stateUpdate.stateSerialization)

        case .accountChange(let event):
            handleAccountChange(event)

        case .fetchedDatabaseChanges(let changes):
            handleDatabaseChanges(changes)

        case .fetchedRecordZoneChanges(let changes):
            handleZoneChanges(changes)

        case .sentDatabaseChanges(let sentChanges):
            handleSentDatabaseChanges(sentChanges)

        case .sentRecordZoneChanges(let sentChanges):
            handleSentZoneChanges(sentChanges)

        default: break
        }
    }

    func nextRecordZoneChangeBatch(
        _ context: CKSyncEngine.SendChangesContext,
        syncEngine: CKSyncEngine
    ) async -> CKSyncEngine.RecordZoneChangeBatch? {
        let pendingChanges = syncEngine.state.pendingRecordZoneChanges
        return await CKSyncEngine.RecordZoneChangeBatch(
            pendingChanges: pendingChanges
        ) { recordID in
            // Convert local record to CKRecord
            self.recordForID(recordID)
        }
    }
}
```

### Conflict Resolution Strategies

```swift
// Strategy 1: Last-Writer-Wins (simplest)
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
    if local.modificationDate ?? .distantPast > server.modificationDate ?? .distantPast {
        return local
    }
    return server
}

// Strategy 2: Field-Level Merge
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
    let merged = server  // Start with server version
    // Merge specific fields from local if newer
    if let localTitle = local["title"], let serverTitle = server["title"] {
        // Keep whichever was modified more recently per-field
        merged["title"] = localTitle  // App-specific logic
    }
    return merged
}

// Strategy 3: User Choice
func resolveConflict(local: CKRecord, server: CKRecord) async -> CKRecord {
    let choice = await showConflictUI(local: local, server: server)
    return choice == .keepLocal ? local : server
}
```

### CloudKit CRUD Operations

```swift
let database = CKContainer.default().privateCloudDatabase

// Create
let record = CKRecord(recordType: "Track")
record["title"] = "Song" as CKRecordValue
record["artist"] = "Artist" as CKRecordValue
try await database.save(record)

// Read
let recordID = CKRecord.ID(recordName: "trackID")
let record = try await database.record(for: recordID)

// Update (fetch-then-modify to avoid conflicts)
let record = try await database.record(for: recordID)
record["playCount"] = (record["playCount"] as? Int ?? 0) + 1
try await database.save(record)

// Delete
try await database.deleteRecord(withID: recordID)

// Batch
let (saved, deleted) = try await database.modifyRecords(
    saving: recordsToSave,
    deleting: recordIDsToDelete,
    savePolicy: .changedKeys  // Only send changed fields
)
```

### CloudKit Database Scopes

```swift
let container = CKContainer.default()

// Private: User's own data, counts toward user's iCloud quota
let privateDB = container.privateCloudDatabase

// Public: Shared across all users, counts toward app's quota
let publicDB = container.publicCloudDatabase

// Shared: Data shared via CKShare, counts toward owner's quota
let sharedDB = container.sharedCloudDatabase
```

### CloudKit Sharing

```swift
// Create share
let share = CKShare(rootRecord: record)
share[CKShare.SystemFieldKey.title] = "Shared Playlist" as CKRecordValue
share.publicPermission = .none  // Invite only

try await database.modifyRecords(saving: [record, share], deleting: [])

// Share UI
let sharingController = UICloudSharingController(share: share, container: container)
present(sharingController, animated: true)

// Accept share (in SceneDelegate or AppDelegate)
func userDidAcceptCloudKitShare(_ cloudKitShareMetadata: CKShare.Metadata) {
    let acceptOp = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
    acceptOp.qualityOfService = .userInitiated
    CKContainer(identifier: cloudKitShareMetadata.containerIdentifier)
        .add(acceptOp)
}

// Participant management
share.addParticipant(participant)
participant.permission = .readWrite
```

### CloudKit Subscriptions

```swift
// Database subscription (all changes in zone)
let subscription = CKDatabaseSubscription(subscriptionID: "all-changes")
subscription.notificationInfo = CKSubscription.NotificationInfo()
subscription.notificationInfo?.shouldSendContentAvailable = true  // Silent push
try await database.save(subscription)

// Query subscription (specific record type)
let querySubscription = CKQuerySubscription(
    recordType: "Track",
    predicate: NSPredicate(format: "artist == %@", "Artist"),
    subscriptionID: "artist-tracks",
    options: [.firesOnRecordCreation, .firesOnRecordUpdate]
)
try await database.save(querySubscription)
```

### CKAsset (File Storage in CloudKit)

```swift
// Upload
let fileURL = URL(fileURLWithPath: "/path/to/image.jpg")
let asset = CKAsset(fileURL: fileURL)
record["coverArt"] = asset

// Download
if let asset = record["coverArt"] as? CKAsset,
   let url = asset.fileURL {
    let data = try Data(contentsOf: url)
}
```

---

### iCloud Drive Setup

```swift
// Entitlements: iCloud Documents capability + ubiquity container

// Get container URL
guard let containerURL = FileManager.default.url(
    forUbiquityContainerIdentifier: "iCloud.com.myapp"
) else {
    // iCloud not available
    return
}

let documentsURL = containerURL.appendingPathComponent("Documents")
try FileManager.default.createDirectory(
    at: documentsURL,
    withIntermediateDirectories: true
)
```

### NSFileCoordinator (Required for iCloud Files)

```swift
// Read with coordination
let coordinator = NSFileCoordinator()
var error: NSError?

coordinator.coordinate(readingItemAt: fileURL, options: [], error: &error) { readURL in
    let data = try? Data(contentsOf: readURL)
    // Process data
}

// Write with coordination
coordinator.coordinate(writingItemAt: fileURL, options: .forReplacing, error: &error) { writeURL in
    try? data.write(to: writeURL)
}

// Move with coordination (both source and destination)
coordinator.coordinate(
    writingItemAt: sourceURL, options: .forMoving,
    writingItemAt: destURL, options: .forReplacing,
    error: &error
) { safeSource, safeDest in
    try? FileManager.default.moveItem(at: safeSource, to: safeDest)
}
```

### NSFilePresenter (Monitor Changes)

```swift
class DocumentPresenter: NSObject, NSFilePresenter {
    let presentedItemURL: URL?
    let presentedItemOperationQueue = OperationQueue()

    init(url: URL) {
        self.presentedItemURL = url
        super.init()
        NSFileCoordinator.addFilePresenter(self)
    }

    func presentedItemDidChange() {
        // File was modified externally (another device)
        reloadContent()
    }

    func presentedItemDidMoveToURL(_ newURL: URL) {
        // File was moved/renamed
    }

    deinit {
        NSFileCoordinator.removeFilePresenter(self)
    }
}
```

### iCloud Drive Conflict Resolution

```swift
func resolveConflicts(for url: URL) throws {
    guard let versions = NSFileVersion.unresolvedConflictVersionsOfItem(at: url),
          !versions.isEmpty else { return }

    let currentVersion = NSFileVersion.currentVersionOfItem(at: url)

    for version in versions {
        if shouldKeep(version, over: currentVersion) {
            try version.replaceItem(at: url)
        }
        version.isResolved = true
    }

    try NSFileVersion.removeOtherVersionsOfItem(at: url)
}
```

### Download & Upload Status

```swift
// Check file status
let values = try url.resourceValues(forKeys: [
    .ubiquitousItemDownloadingStatusKey,
    .ubiquitousItemIsUploadingKey,
    .ubiquitousItemUploadingErrorKey
])

// Trigger download
if values.ubiquitousItemDownloadingStatus == .notDownloaded {
    try FileManager.default.startDownloadingUbiquitousItem(at: url)
}

// Monitor with NSMetadataQuery
let query = NSMetadataQuery()
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
query.predicate = NSPredicate(format: "%K LIKE '*.txt'", NSMetadataItemFSNameKey)

NotificationCenter.default.addObserver(
    forName: .NSMetadataQueryDidUpdate, object: query, queue: .main
) { _ in
    // Handle file changes
}
query.start()
```

### NSUbiquitousKeyValueStore (Small Data)

```swift
// Key-value storage (<1MB total, 1024 keys max)
let kvStore = NSUbiquitousKeyValueStore.default

// Write
kvStore.set("value", forKey: "preference")
kvStore.synchronize()  // Request sync (not guaranteed immediate)

// Read
let value = kvStore.string(forKey: "preference")

// Observe changes
NotificationCenter.default.addObserver(
    forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
    object: kvStore, queue: .main
) { notification in
    guard let userInfo = notification.userInfo,
          let reason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int
    else { return }

    switch reason {
    case NSUbiquitousKeyValueStoreServerChange:
        // External change - update UI
        break
    case NSUbiquitousKeyValueStoreQuotaViolationChange:
        // Over 1MB limit - reduce stored data
        break
    default: break
    }
}
```

### Entitlements Checklist

```
CloudKit:
☐ iCloud capability enabled in Signing & Capabilities
☐ CloudKit checkbox selected
☐ Container identifier configured (iCloud.com.yourapp)
☐ Push Notifications capability (for subscriptions)

iCloud Drive:
☐ iCloud capability enabled
☐ iCloud Documents checkbox selected
☐ Ubiquity container identifier in entitlements
☐ NSUbiquitousContainers in Info.plist (for Files app visibility)

Both:
☐ Signed with provisioning profile that includes iCloud
☐ Real device for testing (simulator has limitations)
☐ iCloud account signed in on device
```

## Decision Tree

```
Cloud storage task?

├─ Choosing sync approach
│   ├─ Simple structured data? → SwiftData + CloudKit
│   ├─ Custom sync logic needed? → CKSyncEngine (WWDC 2023)
│   ├─ Large files/documents? → iCloud Drive
│   ├─ Small preferences? → NSUbiquitousKeyValueStore
│   └─ Complex sharing needs? → CKShare + raw CloudKit
│
├─ Conflict resolution
│   ├─ Structured data? → Field-level merge or last-writer-wins
│   ├─ Documents? → NSFileVersion resolution
│   └─ User-facing? → Present choice UI
│
├─ Sync not working
│   ├─ Check account status first (see Diagnostics)
│   ├─ Check entitlements
│   ├─ Check network
│   └─ Check CloudKit Console for errors
│
└─ Performance
    ├─ Initial sync slow? → Batch fetch with CKQueryOperation
    ├─ Too much data? → Use CKSyncEngine zones for partitioning
    └─ Large files? → CKAsset for CloudKit, file coordination for iCloud Drive
```

## Anti-Patterns

```swift
// ---- Requiring network for save ----
// WRONG: Save fails when offline
func save(_ track: Track) async throws {
    try await cloudDatabase.save(CKRecord(track))  // Fails offline!
}
// FIX: Offline-first - save locally, queue sync
func save(_ track: Track) throws {
    try localDatabase.save(track)
    syncEngine.queueChange(.save(track))
}

// ---- Overwriting without fetch (serverRecordChanged) ----
// WRONG: Save without checking server version
let record = CKRecord(recordType: "Track")
record["title"] = "My Title"
try await database.save(record)  // CKError.serverRecordChanged!
// FIX: Fetch-then-modify
let existing = try await database.record(for: recordID)
existing["title"] = "My Title"
try await database.save(existing)

// ---- Ignoring batch operation partial failures ----
// WRONG: Assuming all records saved
let (saved, _) = try await database.modifyRecords(saving: records, deleting: [])
// Some records may have failed silently!
// FIX: Check per-record results
// Use perRecordSaveBlock or check saved count vs input count

// ---- File access without coordination ----
// WRONG: Direct file access for iCloud files
let data = try Data(contentsOf: iCloudURL)  // May read partial/corrupt data
// FIX: Use NSFileCoordinator
let coordinator = NSFileCoordinator()
coordinator.coordinate(readingItemAt: iCloudURL, options: [], error: &error) { url in
    let data = try Data(contentsOf: url)
}

// ---- NSUbiquitousKeyValueStore for large data ----
// WRONG: Storing large data in key-value store
kvStore.set(largeImageData, forKey: "avatar")  // > 1MB limit!
// FIX: Use CloudKit CKAsset or iCloud Drive for large data
```

## Deep Patterns

### Sync State Indicators

```swift
@MainActor
class SyncStatusViewModel: ObservableObject {
    enum SyncState {
        case idle, syncing, error(String), offline
    }

    @Published var state: SyncState = .idle

    func updateFromEngine(_ event: CKSyncEngine.Event) {
        switch event {
        case .willFetchChanges: state = .syncing
        case .didFetchChanges: state = .idle
        case .willSendChanges: state = .syncing
        case .didSendChanges: state = .idle
        default: break
        }
    }
}
```

### Large Dataset Initial Sync

```swift
// Batch initial fetch
func performInitialSync() async throws {
    var cursor: CKQueryOperation.Cursor? = nil

    repeat {
        let (results, nextCursor) = try await database.records(
            matching: CKQuery(recordType: "Track", predicate: NSPredicate(value: true)),
            resultsLimit: 200,
            desiredKeys: ["title", "artist"],  // Fetch only needed fields
            continuationCursor: cursor
        )

        // Process batch
        let tracks = results.compactMap { try? $0.1.get() }
        try localDatabase.batchInsert(tracks.map(Track.init))

        cursor = nextCursor
    } while cursor != nil
}
```

## Diagnostics

### Mandatory First Checks

```swift
// 1. Account status
func checkAccountStatus() async throws -> CKAccountStatus {
    try await CKContainer.default().accountStatus()
    // .available, .noAccount, .restricted, .couldNotDetermine
}

// 2. Entitlements
// Build Settings → Signing & Capabilities → iCloud

// 3. Network reachability
// NWPathMonitor for network status

// 4. Container accessible
func checkContainer() async throws {
    let container = CKContainer.default()
    let id = try await container.containerIdentifier
    print("Container: \(id ?? "none")")
}
```

### CloudKit Error Handling

```
CKError received?

├─ .serverRecordChanged
│   └─ Conflict: fetch server record, merge, retry save
│       let serverRecord = error.serverRecord!
│       // Merge and retry
│
├─ .quotaExceeded
│   └─ User's iCloud full → show alert, reduce data
│
├─ .networkUnavailable / .networkFailure
│   └─ Queue for retry when network returns
│
├─ .notAuthenticated
│   └─ No iCloud account → prompt user to sign in
│
├─ .zoneNotFound
│   └─ Create zone first, then retry
│
├─ .limitExceeded
│   └─ Too many records in batch → reduce batch size
│
├─ .partialFailure
│   └─ Check perRecordErrors in userInfo
│       let partialErrors = error.partialErrorsByItemID
│
└─ .requestRateLimited
    └─ Retry after error.retryAfterSeconds
```

### iCloud Drive Issues

```
iCloud Drive not syncing?

├─ File not appearing on other devices
│   ├─ Check ubiquitous container URL not nil
│   ├─ File in correct subdirectory? (Documents/)
│   ├─ Using NSFileCoordinator for writes?
│   └─ Check upload status with resource values
│
├─ Download stuck
│   ├─ Check downloading status key
│   ├─ Network available?
│   ├─ Try startDownloadingUbiquitousItem again
│   └─ Check device storage space
│
├─ Conflicts appearing
│   ├─ Multiple devices editing same file simultaneously
│   ├─ Implement NSFileVersion conflict resolution
│   └─ Consider locking mechanism for collaborative editing
│
└─ NSFileCoordinator deadlock
    ├─ Coordinating on main thread? → Move to background
    ├─ Nested coordination? → Flatten to single coordination
    └─ NSFilePresenter callback causing re-entrant coordination?
        → Use separate OperationQueue
```

### Debug Logging

```
// CloudKit
-com.apple.CoreData.CloudKitDebug 1

// Verbose CloudKit
-com.apple.CoreData.CloudKitDebug 3

// CloudKit Console (web)
// https://icloud.developer.apple.com/dashboard
// Monitor: errors, throughput, sync state
```

## Related

- `ax-swiftdata` - SwiftData + CloudKit container setup
- `ax-grdb` - SQLiteData + CKSyncEngine integration
- `ax-file-storage` - Local file storage decisions before cloud sync
- `ax-concurrency` - Background sync operations

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.