mcpField
A
axiom-cloudkit-ref

axiom-cloudkit-ref

github.com/charleswiltgenデータ・データベース1.2k194 回インストール今日更新
リポジトリ

axiom-cloudkit-ref について

axiom-cloudkit-refは、構造化データを同期するiOS開発者向けのCloudKitリファレンススキルです。SwiftData、CKSyncEngine、CloudKit APIの選択と実装に加え、競合、共有、監視を解説します。

主な機能

  1. 013種類のCloudKit同期方式の比較
  2. 02プライベート・パブリック・共有データベースの設定
  3. 03同期状態と競合の処理
  4. 04レコード共有とプッシュ通知の設定
  5. 05リクエスト・エラー・遅延の監視

活用シーン

  1. 01SwiftDataアプリのiCloud同期設定
  2. 02独自の永続化データのクラウド同期
  3. 03データベース範囲の選択と共同作業の実装
  4. 04同期の問題調査と性能データの確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-cloudkit-ref

インストール後、Claude Code で/axiom-cloudkit-refを入力して読み込まれたか確認します。

詳細

CloudKit リファレンス

目的:データベースベースの iCloud ストレージと同期に関する CloudKit の包括的なリファレンス 利用可能環境:iOS 10.0 以降(基本機能)、iOS 17.0 以降(CKSyncEngine)、iOS 17.0 以降(SwiftData 統合) コンテキスト:CKSyncEngine(WWDC 2023)または SwiftData 統合を使った最新の CloudKit 同期

このスキルを使う場面

次のような場合にこのスキルを使います:

  • iCloud への構造化データの同期を実装する
  • SwiftData+CloudKit、CKSyncEngine、または CloudKit の生 API のどれを使うか選ぶ
  • パブリック/プライベート/共有データベースを設定する
  • 競合解決を実装する
  • CloudKit の同期に関する問題をデバッグする
  • CloudKit のパフォーマンスを監視する

対象外:単純なファイル同期(代わりに axiom-icloud-drive-ref を使ってください)

概要

CloudKit は構造化データ(関連付けを持つレコード)の同期向けであり、単純なファイル同期向けではありません。

最新の方法は 3 つあります:

  1. SwiftData + CloudKit(最も簡単、iOS 17 以降)
  2. CKSyncEngine(カスタム永続化、iOS 17 以降、WWDC 2023)
  3. CloudKit の生 API(最大限の制御が可能ですが、複雑さも増します)

方法 1:SwiftData + CloudKit(推奨)

使用場面:SwiftData モデルを使う iOS 17 以降のアプリ

制限事項:

  • プライベートデータベースのみ(パブリック/共有は不可)
  • 同期は自動(制御性は低め)
  • CloudKit 同期では @Attribute(.unique) はサポートされません。CloudKit を使う場合は削除してください
  • SwiftData の制約が適用されます
// ✅ 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

必要なエンタイトルメント:

  • iCloud 機能
  • CloudKit コンテナ

SwiftData の詳細については axiom-swiftdata スキルを使ってください


方法 2:CKSyncEngine(最新、WWDC 2023)

使用場面:クラウド同期を備えたカスタム永続化(SQLite、GRDB、JSON)

CloudKit の生 API と比べた利点:

  • フェッチ/アップロードのサイクルを自動的に管理します
  • 競合を処理します
  • アカウントの変更を管理します
  • CKDatabase の操作を手動で行うより推奨されます
// ✅ 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: []
        )
    }
}

主な概念:

  • 状態のシリアライズ:アプリの起動間で同期状態を永続化します
  • イベント:デリゲートが変更に関するイベントを受け取ります
  • バッチ:保留中の変更を渡すと、エンジンがアップロードします
  • 自動競合解決:エンジンが基本的な競合を処理します

方法 3:CloudKit の生 API(レガシー)

使用場面:CKSyncEngine が適合しない場合に限ります(まれです)

主要な型:

  • CKContainer — エントリーポイント
  • CKDatabase — パブリック/プライベート/共有のスコープ
  • CKRecord — 個々のデータレコード
  • CKRecordZone — 論理的なグループ化
  • CKAsset — バイナリファイルのストレージ
基本操作
// ✅ 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)
レコードの更新
// ✅ 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)
競合解決
// ✅ 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)

データベースのスコープ

スコープアクセス範囲SwiftData のサポート用途
プライベートユーザー本人のみ✅ はい個人のユーザーデータ
パブリックすべてのユーザー❌ いいえ共有/パブリックコンテンツ
共有招待されたユーザー❌ いいえコラボレーション
プライベートデータベース
// ✅ 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 (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 (collaboration)
let sharedDB = CKContainer.default().sharedCloudDatabase

// For CKShare-based collaboration
// Users invited to specific record zones
// Use for: Shared documents, team data

CloudKit アセット(ファイル)

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

重要:CKAsset には Data ではなく、ファイル URL が必要です。まずデータを一時ファイルに書き込んでください。


CloudKit Console(監視 — WWDC 2024)

デベロッパー通知

次の項目についてアラートを設定します:

  • スキーマの変更
  • クォータ超過
  • 高いエラー率
  • カスタムしきい値
テレメトリー

次の項目を監視します:

  • リクエスト数
  • エラー率
  • レイテンシー(p50、p95、p99)
  • 帯域幅の使用量
ログ

次の情報を確認できます:

  • 個々のリクエスト
  • エラーの詳細
  • パフォーマンスのボトルネック

アクセス:https://icloud.developer.apple.com/dashboard


一般的なパターン

パターン 1:初回同期
// ✅ 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)
        }
    }
}
パターン 2:差分同期
// ✅ 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)
}

エンタイトルメント

Xcode で必要なエンタイトルメント:


<key>com.apple.developer.icloud-services</key>
<array>
    <string>CloudKit</string>
</array>


<key>com.apple.developer.icloud-container-identifiers</key>
<array>
    <string>iCloud.com.example.app</string>
</array>

設定方法:

  1. Xcode → ターゲット → 署名と機能
  2. 「+ 機能」→ iCloud
  3. 「CloudKit」にチェックを入れる
  4. コンテナを選択または作成する

サブスクリプション(プッシュ通知)

データベースサブスクリプション
// ✅ 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)
クエリーサブスクリプション
// ✅ 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)
ゾーンサブスクリプション
// ✅ 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)
プッシュ通知の処理
// 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
}

レコードの共有

共有を作成する
// ✅ 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)
共有 UI を表示する
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"
    }
}
参加者を管理する
// ✅ 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)
共有を承諾する
// 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)
}

クイックリファレンス

タスクモダン API(iOS 17+)レガシー API
構造化データの同期SwiftData + CloudKitCKSyncEngine または CKDatabase
カスタム永続化の同期CKSyncEngineCKDatabase
競合解決自動(SwiftData/CKSyncEngine)手動(savePolicy)
アカウントの変更自動的に処理手動で検出
監視CloudKit Console のテレメトリ手動ログ

関連スキル

  • axiom-swiftdata — SwiftData の実装詳細
  • axiom-storage — CloudKit と iCloud Drive の選択
  • axiom-icloud-drive-ref — ファイルベースの iCloud 同期
  • axiom-cloud-sync-diag — CloudKit 同期の問題をデバッグ

最終更新: 2025-12-12 スキルの種類: リファレンス 最小 iOS: 10.0(基本)、17.0(CKSyncEngine、SwiftData 統合) WWDC セッション: 2023-10188(CKSyncEngine)、2024-10122(CloudKit Console)

コメント