取り組みの背景
iOS アプリで「ユーザーの複数デバイス間でデータを同期したい」というニーズは非常に多いですが、CloudKit の実装は複雑でデバッグにも時間がかかりがちです。ここでは Antigravity IDE のエージェントを活用して、SwiftUI + CloudKit によるデバイス間リアルタイム同期アプリを本番レベルで構築する方法を解説します。
本記事の対象読者:
- iOS 開発経験があり、CloudKit を使ったことがない・難しさを感じている方
- Antigravity による iOS 開発効率化に関心がある方
- SwiftUI + CloudKit の構成でアプリを本番リリースしたい方
本記事を読み終えると、以下が実装できるようになります:
- CloudKit コンテナのセットアップと CKRecord モデル設計
@Observable+ CloudKit によるリアルタイムデータ同期- オフライン対応とコンフリクト(競合)解決ロジック
CKSubscriptionを使ったサイレントプッシュ通知連携
前提知識・環境構築
必要な環境
- Xcode 26(macOS 15.x Sequoia 以上)
- Apple Developer Program 登録済み
- Antigravity 最新版(1.20 以降)
- iOS 18 以上を対象
Xcode でのセットアップ
まず Antigravity で新規 iOS プロジェクトを作成し、エージェントに以下の指示を出します:
Antigravity プロンプト例:
「SwiftUI を使った iOS アプリを作成してください。iCloud の CloudKit を使ってタスクデータを
デバイス間で同期します。Signing & Capabilities に CloudKit と Push Notifications を追加し、
com.example.myapp.cloudkit という名前のコンテナを設定してください。」
Antigravity は次の手順を自動実行します:
CloudKitケイパビリティを Xcode プロジェクトに追加Push Notificationsケイパビリティを追加Info.plistに必要なエントリを追記- CloudKit コンテナ識別子を
.entitlementsに設定
Xcode 26 では、Signing & Capabilities の設定が JSON ベースになり、Antigravity エージェントが直接ファイル編集できるようになりましました。従来は GUI 操作が必要ですったため、この変更によって自動化の精度が大幅に向上しています。
CloudKit データモデルの設計思想
Record Type のスキーマ設計
CloudKit では RDB のテーブルに相当する「Record Type」を定義します。Antigravity のエージェントに次のようにスキーマ設計を依頼できます:
「タスク管理アプリ用の CloudKit スキーマを設計してください。Task、Tag、Comment の
3つの Record Type を作成し、それぞれのフィールドとリレーションを
CloudKit Dashboard に入力できる形式で出力してください。」
エージェントが生成するスキーマ例:
| Record Type | Field | Type |
|---|---|---|
| Task | title | String |
| Task | isCompleted | Int(64) |
| Task | dueDate | Date/Time |
| Task | priority | Int(64) |
| Task | tagReference | Reference |
| Tag | name | String |
| Tag | color | String |
| Comment | body | String |
| Comment | taskReference | Reference |
| Comment | createdAt | Date/Time |
データベースゾーンの選択
CloudKit の権限管理は「Public / Private / Shared」の3ゾーンで行います。
- Private Database:ユーザー固有データの保存に使用。ユーザー自身の iCloud ストレージを消費するため、開発者に追加コストなし
- Shared Database:チームや家族間でのデータ共有に使用(iOS 15 以降)
- Public Database:全ユーザーが読み取れるグローバルデータ(App Store 公開コンテンツなど)
コード例 1:CKRecord データストアの実装
以下は @Observable マクロを使った CloudKit データストアの完全実装です。Antigravity に「CKRecord を使った Observable データストアを Swift Concurrency で実装してください」と指示することで生成できます。
import CloudKit
import Observation
// CloudKit コンテナ識別子(プロジェクトに合わせて変更)
private let containerIdentifier = "iCloud.com.example.myapp.cloudkit"
// Task の CloudKit Record Type 定数
enum RecordType {
static let task = "Task"
static let tag = "Tag"
}
@Observable
final class TaskStore {
var tasks: [TaskItem] = []
var isLoading = false
var errorMessage: String?
private let container: CKContainer
private let privateDB: CKDatabase
init() {
self.container = CKContainer(identifier: containerIdentifier)
self.privateDB = container.privateCloudDatabase
}
// --- データ取得(一覧) ---
func fetchTasks() async {
isLoading = true
defer { isLoading = false }
do {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: RecordType.task, predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
let results = try await privateDB.records(matching: query)
let records = try results.matchResults.compactMap { _, result in
try result.get()
}
await MainActor.run {
self.tasks = records.compactMap(TaskItem.init)
}
} catch {
await MainActor.run {
self.errorMessage = error.localizedDescription
}
}
}
// --- 新規タスクの保存 ---
func saveTask(_ item: TaskItem) async throws {
let record = item.toCKRecord()
let savedRecord = try await privateDB.save(record)
let savedItem = TaskItem(record: savedRecord)!
await MainActor.run {
if let index = self.tasks.firstIndex(where: { $0.id == item.id }) {
self.tasks[index] = savedItem
} else {
self.tasks.insert(savedItem, at: 0)
}
}
}
}
// --- TaskItem モデル ---
struct TaskItem: Identifiable, Hashable {
let id: CKRecord.ID
var title: String
var isCompleted: Bool
var dueDate: Date?
var priority: Int
// CKRecord から初期化
init?(record: CKRecord) {
guard let title = record["title"] as? String else { return nil }
self.id = record.recordID
self.title = title
self.isCompleted = (record["isCompleted"] as? Int64 ?? 0) == 1
self.dueDate = record["dueDate"] as? Date
self.priority = Int(record["priority"] as? Int64 ?? 0)
}
// CKRecord へ変換
func toCKRecord() -> CKRecord {
let record = CKRecord(recordType: RecordType.task, recordID: id)
record["title"] = title
record["isCompleted"] = isCompleted ? 1 : 0
record["dueDate"] = dueDate
record["priority"] = Int64(priority)
return record
}
}このコードのポイントは3つです。まず、Swift Concurrency(async/await)で CloudKit の非同期 API をシンプルに扱えています。次に、@Observable マクロにより SwiftUI ビューが自動的に変更を検知して再レンダリングされます。そして CKRecord.ID を TaskItem.id として使うことで、CloudKit との ID 整合性を保っています。
コード例 2:SwiftUI リアルタイム同期ビューの実装
次は、TaskStore を使った SwiftUI ビューと、CKSubscription による他デバイスからの変更検知を実装します。
import SwiftUI
import CloudKit
// --- タスク一覧ビュー ---
struct TaskListView: View {
@State private var store = TaskStore()
@State private var showAddTask = false
var body: some View {
NavigationStack {
Group {
if store.isLoading && store.tasks.isEmpty {
ProgressView("読み込み中...")
} else {
List {
ForEach(store.tasks) { task in
TaskRowView(task: task) { updated in
Task { try? await store.saveTask(updated) }
}
}
}
}
}
.navigationTitle("タスク")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button { showAddTask = true } label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddTask) {
AddTaskView { newTask in
Task { try? await store.saveTask(newTask) }
}
}
.task {
// 初回データ取得
await store.fetchTasks()
// CKSubscription でリアルタイム変更を購読
await store.subscribeToChanges()
}
.onReceive(NotificationCenter.default.publisher(
for: .CKAccountChanged
)) { _ in
// iCloud アカウント変更時に再取得
Task { await store.fetchTasks() }
}
}
}
}
// --- TaskStore に購読機能を追加 ---
extension TaskStore {
func subscribeToChanges() async {
let subscriptionID = "task-changes"
// 既存の購読を確認(重複登録防止)
do {
_ = try await privateDB.subscription(for: subscriptionID)
print("✅ 既存の CKSubscription を確認")
} catch {
// 未登録の場合は新規作成
let subscription = CKQuerySubscription(
recordType: RecordType.task,
predicate: NSPredicate(value: true),
subscriptionID: subscriptionID,
options: [
.firesOnRecordCreation,
.firesOnRecordUpdate,
.firesOnRecordDeletion
]
)
// サイレントプッシュ設定(バックグラウンドでデータ取得)
let notificationInfo = CKSubscription.NotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
do {
try await privateDB.save(subscription)
print("✅ CKSubscription 登録完了")
} catch {
print("⚠️ CKSubscription 登録エラー: \(error.localizedDescription)")
}
}
}
}shouldSendContentAvailable = true を設定することで、バックグラウンドでの「サイレントプッシュ」が有効になります。他デバイスでタスクが更新された際に、アプリがバックグラウンドで自動的にデータを再取得する仕組みです。ユーザーが通知を受け取ることはなく、次にアプリを開いた際には最新データが表示されます。
コード例 3:オフライン対応とコンフリクト解決
CloudKit でよく発生する問題が「コンフリクト(競合)エラー」です。複数デバイスで同じレコードを同時編集した場合、CKError.serverRecordChanged が発生します。Antigravity で「CloudKit のコンフリクト解決ロジックを実装してください」と指示すると次のようなコードが生成されます。
extension TaskStore {
// --- コンフリクト対応付きの保存 ---
func saveWithConflictResolution(_ item: TaskItem) async throws {
do {
try await saveTask(item)
} catch let error as CKError where error.code == .serverRecordChanged {
// サーバー側の最新レコードを取得
guard
let serverRecord = error.userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord,
let clientRecord = error.userInfo[CKRecordChangedErrorClientRecordKey] as? CKRecord
else { throw error }
// マージ戦略:
// - タイトルはクライアント(ユーザーが今編集中)を優先
// - 完了状態はサーバー最新値を維持(他デバイスでの完了を尊重)
let mergedRecord = serverRecord
mergedRecord["title"] = clientRecord["title"]
// isCompleted はサーバー値をそのまま使用
let savedRecord = try await privateDB.save(mergedRecord)
let savedItem = TaskItem(record: savedRecord)!
await MainActor.run {
if let index = self.tasks.firstIndex(where: { $0.id == item.id }) {
self.tasks[index] = savedItem
}
}
}
}
// --- ネットワーク不通時のオフライン待機 ---
func saveOfflineIfNeeded(_ item: TaskItem) async {
do {
try await saveWithConflictResolution(item)
} catch let error as CKError
where error.code == .networkUnavailable || error.code == .networkFailure
{
// オフライン時はローカルキューに追加
PendingChangesQueue.shared.enqueue(item)
print("⚡ オフラインキューに追加: \(item.title)(待機件数: \(PendingChangesQueue.shared.count))")
} catch {
// その他のエラーは UI に表示
await MainActor.run {
self.errorMessage = error.localizedDescription
}
}
}
}
// --- オフライン待機キュー ---
final class PendingChangesQueue {
static let shared = PendingChangesQueue()
private var queue: [TaskItem] = []
var count: Int { queue.count }
func enqueue(_ item: TaskItem) {
// 同一 ID の既存エントリを上書き(最新変更のみ保持)
queue.removeAll { $0.id == item.id }
queue.append(item)
}
// ネットワーク復帰時に一括フラッシュ
func flushAll(to store: TaskStore) async {
let pending = queue
queue.removeAll()
for item in pending {
try? await store.saveWithConflictResolution(item)
}
print("✅ オフラインキューをフラッシュ完了(\(pending.count) 件)")
}
}本番アプリでは PendingChangesQueue を SwiftData または Core Data に永続化することを推奨します。アプリ終了・再起動をまたいでオフライン変更を保持できます。
ネットワーク状態の監視は Network.framework の NWPathMonitor と組み合わせ、接続復帰時に自動で flushAll を呼び出す設計が一般的です。
Antigravity での開発効率化ポイント
Manager Surface でのマルチエージェント並列活用
Antigravity の Manager Surface を使うと、複数のエージェントを並列実行できます。CloudKit 統合では次のような分業が効果的です:
- エージェント A:CloudKit スキーマ設計と
CKRecordモデル生成 - エージェント B:SwiftUI ビュー層の実装(
TaskListView,AddTaskView等) - エージェント C:単体テスト(XCTest)の自動生成と CloudKit モックの作成
この並列アプローチにより、従来2〜3日かかっていた CloudKit 統合作業を数時間に短縮できます。
AGENTS.md でコンテキストを共有する
プロジェクトルートの AGENTS.md に CloudKit のコンテナ識別子、スキーマ設計、アーキテクチャの決定事項を記述することで、すべてのエージェントが同一コンテキストで作業します。Antigravity SwiftUI スキルガイド に AGENTS.md のテンプレートが掲載されています。
iOS 26 新 API への対応
iOS 26・Xcode 26 移行ガイド と組み合わせることで、iOS 26 で追加・変更された CloudKit 関連 API へのスムーズな移行が可能です。Antigravity エージェントは最新の iOS SDK 情報を保持しており、旧 API の使用を検出すると自動的に最新版への書き換えを提案します。
トラブルシューティング
よくあるエラーと対処法
CKError.notAuthenticated — iCloud にサインインしていない
// アプリ起動時に iCloud ログイン状態を確認
let status = try await CKContainer.default().accountStatus()
if status != .available {
// ユーザーに iCloud サインインを促すアラートを表示
showICloudSignInAlert = true
}CKError.quotaExceeded — ストレージ上限超過
ユーザーの iCloud ストレージが不足しています。画像や動画などの大容量アセットは CKAsset で管理し、テキストデータと分けて設計することで容量を最適化できます。また、使用済みデータの定期的なクリーンアップ機能を実装することを推奨します。
CKError.partialFailure — 一部レコードの保存失敗
error.userInfo[CKPartialErrorsByItemIDKey] でどのレコードが失敗したかを確認し、失敗したレコードのみ再試行する設計にしてください。全件再試行すると正常に保存済みのデータを上書きするリスクがあります。
プッシュ通知が届かない
Push Notificationsケイパビリティが追加されているか確認- Simulator ではサイレントプッシュが動作しないため、実機テストが必須
- 開発環境と本番環境で APN エンドポイントが異なることに注意
- TestFlight ビルドは本番 APN を使用するため、リリース前に必ず TestFlight でのテストを実施
パフォーマンス考慮事項
ページングクエリで大量データを効率処理
CloudKit では一度のクエリで取得できるレコード数に上限があります(デフォルト 400 件)。大量データを扱う場合は cursor を使ったページングが必要です:
// ページング対応クエリ(大量データ対応)
func fetchTasksPaged() async throws -> [CKRecord] {
var allRecords: [CKRecord] = []
let query = CKQuery(
recordType: RecordType.task,
predicate: NSPredicate(value: true)
)
var cursor: CKQueryOperation.Cursor? = nil
repeat {
let (results, newCursor) = try await privateDB.records(
matching: query,
resultsLimit: 200,
// cursor が nil の場合は最初から取得
// cursor がある場合は続きから取得(CLoudKit API v2)
)
let batch = try results.matchResults.map { _, r in try r.get() }
allRecords.append(contentsOf: batch)
cursor = newCursor
} while cursor != nil
return allRecords
}
// 期待する出力: allRecords に全タスクが格納される(件数制限なし)デルタ同期で通信量を削減
CKFetchRecordZoneChangesOperation を使うと、前回同期以降に変更されたレコードのみを取得する「デルタ同期」が可能です。全件取得と比べて通信量・処理時間を大幅に削減できます。同期トークン(serverChangeToken)は UserDefaults または SwiftData に保存し、次回起動時に再利用します。
まとめ
ここでは Antigravity を使った SwiftUI + CloudKit の本番実装について解説しました。
@Observable+ CKRecord による型安全でリアクティブなデータストア設計CKSubscriptionサイレントプッシュによるデバイス間リアルタイム同期- コンフリクト解決とオフライン対応のロジック実装
- Manager Surface でのマルチエージェント並列開発による開発時間の大幅短縮
CloudKit は設定の複雑さから敬遠されがちですが、Antigravity のエージェントを活用することで、セットアップから実装・デバッグまで一気通貫に進められます。今回紹介したコード例を起点に、ぜひ自分のアプリに iCloud 同期機能を追加してみてください。