ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-03-18上級

Antigravity × SwiftUI + CloudKit:iCloud 同期アプリを本番実装するアドバンスガイド

SwiftUI と CloudKit を Antigravity で組み合わせてデバイス間リアルタイム同期 iOS アプリを実装。CKRecord 設計からオフライン対応、プッシュ通知連携まで本番レベルで徹底解説します。

SwiftUI9CloudKitiOS27iCloudリアルタイム同期2モバイル開発8Antigravity319

取り組みの背景

iOS アプリで「ユーザーの複数デバイス間でデータを同期したい」というニーズは非常に多いですが、CloudKit の実装は複雑でデバッグにも時間がかかりがちです。ここでは Antigravity IDE のエージェントを活用して、SwiftUI + CloudKit によるデバイス間リアルタイム同期アプリを本番レベルで構築する方法を解説します。

本記事の対象読者:

  • iOS 開発経験があり、CloudKit を使ったことがない・難しさを感じている方
  • Antigravity による iOS 開発効率化に関心がある方
  • SwiftUI + CloudKit の構成でアプリを本番リリースしたい方

本記事を読み終えると、以下が実装できるようになります:

  1. CloudKit コンテナのセットアップと CKRecord モデル設計
  2. @Observable + CloudKit によるリアルタイムデータ同期
  3. オフライン対応とコンフリクト(競合)解決ロジック
  4. 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 TypeFieldType
TasktitleString
TaskisCompletedInt(64)
TaskdueDateDate/Time
TaskpriorityInt(64)
TasktagReferenceReference
TagnameString
TagcolorString
CommentbodyString
CommenttaskReferenceReference
CommentcreatedAtDate/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.IDTaskItem.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) 件)")
    }
}

本番アプリでは PendingChangesQueueSwiftData または Core Data に永続化することを推奨します。アプリ終了・再起動をまたいでオフライン変更を保持できます。

ネットワーク状態の監視は Network.frameworkNWPathMonitor と組み合わせ、接続復帰時に自動で 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 同期機能を追加してみてください。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-05-02
Antigravity × Gemma 4 で作る SwiftUI マルチモーダル画像 AI アプリ実装ガイド — カメラ・フォトライブラリ・テキスト入力を一気通貫で
Antigravity と Gemma 4 を組み合わせ、カメラ・フォトライブラリ・テキスト入力に対応したマルチモーダル AI アプリを SwiftUI で実装する完全ガイド。Core ML 統合からエラーハンドリング、App Store 審査対応まで網羅します。
アプリ開発2026-04-02
AntigravityでARアプリを作る入門ガイド 2026 — ARKit・ARCoreで拡張現実を体験しよう
AntigravityのAIエージェントを活用してARKit(iOS)・ARCore(Android)の拡張現実アプリを効率よく開発する入門ガイド。2026年のAR市場動向からステップバイステップの実装手順まで丁寧に解説します。
アプリ開発2026-03-10
Xcode × Antigravity iOS 開発ガイド — Swift/SwiftUI を AI で加速する
Xcode と Antigravity を組み合わせて iOS/iPadOS アプリ開発を加速する方法を、SwiftUI のコード生成からテスト、本番運用でのハマりどころと回避策まで、5,000万DL の個人開発現場の数字と一緒に共有します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →