CloudKit for iCloud sync

Sofia Martinez Jan 2026
1 tab
import CloudKit
import Combine

class CloudKitManager: ObservableObject {
    static let shared = CloudKitManager()

    private let container: CKContainer
    private let publicDatabase: CKDatabase
    private let privateDatabase: CKDatabase

    @Published var isSignedIn = false

    private init() {
        container = CKContainer.default()
        publicDatabase = container.publicCloudDatabase
        privateDatabase = container.privateCloudDatabase

        checkAccountStatus()
    }

    func checkAccountStatus() {
        container.accountStatus { [weak self] status, error in
            DispatchQueue.main.async {
                self?.isSignedIn = status == .available
            }
        }
    }

    // MARK: - Save Record
    func savePost(title: String, body: String) async throws -> CKRecord {
        let record = CKRecord(recordType: "Post")
        record["title"] = title
        record["body"] = body
        record["createdAt"] = Date()

        return try await privateDatabase.save(record)
    }

    // MARK: - Fetch Records
    func fetchPosts() async throws -> [CKRecord] {
        let query = CKQuery(recordType: "Post", predicate: NSPredicate(value: true))
        query.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]

        let (results, _) = try await privateDatabase.records(matching: query)

        return results.compactMap { _, result in
            try? result.get()
        }
    }

    // MARK: - Query with Predicate
    func searchPosts(query: String) async throws -> [CKRecord] {
        let predicate = NSPredicate(format: "title CONTAINS %@", query)
        let ckQuery = CKQuery(recordType: "Post", predicate: predicate)

        let (results, _) = try await privateDatabase.records(matching: ckQuery)

        return results.compactMap { _, result in
            try? result.get()
        }
    }

    // MARK: - Update Record
    func updatePost(record: CKRecord, title: String, body: String) async throws {
        record["title"] = title
        record["body"] = body
        record["updatedAt"] = Date()

        try await privateDatabase.save(record)
    }

    // MARK: - Delete Record
    func deletePost(recordID: CKRecord.ID) async throws {
        try await privateDatabase.deleteRecord(withID: recordID)
    }

    // MARK: - Subscribe to Changes
    func subscribeToPostChanges() async throws {
        let subscription = CKQuerySubscription(
            recordType: "Post",
            predicate: NSPredicate(value: true),
            options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
        )

        let notification = CKSubscription.NotificationInfo()
        notification.shouldSendContentAvailable = true
        subscription.notificationInfo = notification

        try await privateDatabase.save(subscription)
    }

    // MARK: - Handle Push Notification
    func handleNotification(_ notification: CKNotification) {
        guard let queryNotification = notification as? CKQueryNotification else { return }

        switch queryNotification.queryNotificationReason {
        case .recordCreated:
            print("New record created")
        case .recordUpdated:
            print("Record updated")
        case .recordDeleted:
            print("Record deleted")
        @unknown default:
            break
        }
    }
}

// MARK: - SwiftUI Integration
struct CloudKitPostsView: View {
    @StateObject private var cloudKit = CloudKitManager.shared
    @State private var posts: [CKRecord] = []
    @State private var isLoading = false

    var body: some View {
        List(posts, id: \.recordID) { record in
            VStack(alignment: .leading) {
                Text(record["title"] as? String ?? "")
                    .font(.headline)
                Text(record["body"] as? String ?? "")
                    .font(.subheadline)
            }
        }
        .task {
            await loadPosts()
        }
    }

    private func loadPosts() async {
        isLoading = true
        do {
            posts = try await cloudKit.fetchPosts()
        } catch {
            print("Error loading posts: \(error)")
        }
        isLoading = false
    }
}
1 file · swift Explain with highlit

CloudKit syncs app data across a user's devices via iCloud. Public databases share data with all users, private databases store user-specific data, and shared databases enable collaboration. I define record types with fields in the CloudKit Dashboard, then create CKRecord instances in code. CKContainer provides access to databases, and operations like CKQuery fetch records. Subscriptions push changes to devices. CloudKit JS enables web access. For simple key-value storage, NSUbiquitousKeyValueStore syncs UserDefaults-like data. Core Data with CloudKit integration handles complex models automatically. CloudKit requires iCloud capability and user authentication. Error handling addresses network issues and conflicts.