axiom-cloudkit-ref について
axiom-cloudkit-refは、構造化データを同期するiOS開発者向けのCloudKitリファレンススキルです。SwiftData、CKSyncEngine、CloudKit APIの選択と実装に加え、競合、共有、監視を解説します。
主な機能
- 013種類のCloudKit同期方式の比較
- 02プライベート・パブリック・共有データベースの設定
- 03同期状態と競合の処理
- 04レコード共有とプッシュ通知の設定
- 05リクエスト・エラー・遅延の監視
活用シーン
- 01SwiftDataアプリのiCloud同期設定
- 02独自の永続化データのクラウド同期
- 03データベース範囲の選択と共同作業の実装
- 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 つあります:
- SwiftData + CloudKit(最も簡単、iOS 17 以降)
- CKSyncEngine(カスタム永続化、iOS 17 以降、WWDC 2023)
- 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>
設定方法:
- Xcode → ターゲット → 署名と機能
- 「+ 機能」→ iCloud
- 「CloudKit」にチェックを入れる
- コンテナを選択または作成する
サブスクリプション(プッシュ通知)
データベースサブスクリプション
// ✅ 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 + CloudKit | CKSyncEngine または CKDatabase |
| カスタム永続化の同期 | CKSyncEngine | CKDatabase |
| 競合解決 | 自動(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)