CloudKit en Swift: CKDatabase, CKRecord, consultas y sincronización con iCloud

CloudKit es el servicio de Apple para sincronizar datos con iCloud sin gestionar un servidor propio. Los datos se almacenan en los servidores de Apple y el sistema se encarga de distribuirlos entre los dispositivos del usuario. La integración con iOS es estrecha: autenticación gratuita, cuotas generosas en la base de datos privada y soporte nativo en Core Data con NSPersistentCloudKitContainer.

CKContainer y CKDatabase

El punto de entrada es CKContainer, que contiene tres bases de datos con diferente ámbito de acceso:

import CloudKit

let contenedor = CKContainer.default()

// Base de datos privada: datos del usuario, solo visibles para él
let bbddPrivada = contenedor.privateCloudDatabase

// Base de datos pública: datos visibles para todos los usuarios de la app
let bbddPublica = contenedor.publicCloudDatabase

// Base de datos compartida: para funcionalidades de colaboración (iOS 15+)
let bbddCompartida = contenedor.sharedCloudDatabase

CKRecord: crear y guardar registros

CKRecord es el equivalente a una fila en una tabla. Se identifica por un CKRecord.ID que combina un nombre y una zona:

// Crear un registro
let id = CKRecord.ID(recordName: UUID().uuidString)
let nota = CKRecord(recordType: "Nota", recordID: id)
nota["titulo"] = "Compras" as CKRecordValue
nota["contenido"] = "Leche, pan, huevos" as CKRecordValue
nota["fecha"] = Date() as CKRecordValue

// Guardar en iCloud
bbddPrivada.save(nota) { registro, error in
    if let error {
        print("Error guardando: (error)")
        return
    }
    print("Guardado: (registro?.recordID.recordName ?? "")")
}

CKQuery con NSPredicate: consultas

Las consultas usan CKQuery con predicados de NSPredicate. Los predicados soportados son un subconjunto limitado respecto a Core Data:

// Buscar notas de hoy, ordenadas por fecha
let inicio = Calendar.current.startOfDay(for: Date())
let predicate = NSPredicate(format: "fecha >= %@", inicio as NSDate)
let query = CKQuery(recordType: "Nota", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "fecha", ascending: false)]

bbddPrivada.fetch(withQuery: query, inZoneWith: nil,
                  desiredKeys: ["titulo", "fecha"],
                  resultsLimit: 20) { result in
    switch result {
    case .success(let (matchResults, _)):
        for (_, recordResult) in matchResults {
            if case .success(let record) = recordResult {
                print(record["titulo"] as? String ?? "")
            }
        }
    case .failure(let error):
        print("Error en query: (error)")
    }
}

Paginación con CKQueryOperation

Para resultados paginados cuando hay muchos registros, usa CKQueryOperation con el cursor devuelto por el servidor:

func cargarNotas(cursor: CKQueryOperation.Cursor? = nil) {
    let operacion: CKQueryOperation

    if let cursor {
        operacion = CKQueryOperation(cursor: cursor)
    } else {
        let query = CKQuery(recordType: "Nota", predicate: NSPredicate(value: true))
        operacion = CKQueryOperation(query: query)
    }
    operacion.resultsLimit = 50

    operacion.recordMatchedBlock = { _, result in
        if case .success(let record) = result {
            DispatchQueue.main.async {
                self.notas.append(record)
            }
        }
    }

    operacion.queryResultBlock = { result in
        if case .success(let cursor) = result, let cursor {
            // Hay más resultados: cargar la siguiente página
            self.cargarNotas(cursor: cursor)
        }
    }

    bbddPrivada.add(operacion)
}

Suscripciones push: notificaciones en tiempo real

CloudKit puede notificar a los dispositivos cuando cambian los datos mediante suscripciones push. Esto permite mantener los datos actualizados sin polling:

func suscribirseACambiosDeNotas() {
    let predicate = NSPredicate(value: true)
    let suscripcion = CKQuerySubscription(
        recordType: "Nota",
        predicate: predicate,
        options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
    )

    let notificacion = CKSubscription.NotificationInfo()
    notificacion.shouldSendContentAvailable = true  // silent push
    suscripcion.notificationInfo = notificacion

    bbddPrivada.save(suscripcion) { _, error in
        if let error {
            print("Error creando suscripción: (error)")
        }
    }
}

NSPersistentCloudKitContainer con Core Data

La forma más cómoda de usar CloudKit en nuevos proyectos es con NSPersistentCloudKitContainer, que sincroniza automáticamente Core Data con iCloud:

class PersistenceController {
    static let shared = PersistenceController()
    let container: NSPersistentCloudKitContainer

    init() {
        container = NSPersistentCloudKitContainer(name: "MiModelo")

        let descripcion = container.persistentStoreDescriptions.first!
        descripcion.setOption(true as NSNumber,
                              forKey: NSPersistentHistoryTrackingKey)
        descripcion.setOption(true as NSNumber,
                              forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores { _, error in
            if let error { fatalError("(error)") }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}

Con esta configuración, cualquier cambio que se guarde en el contexto se sincroniza automáticamente con iCloud y se propaga a los demás dispositivos del usuario.

CKAsset: ficheros adjuntos

Para adjuntar ficheros (imágenes, PDFs) a un registro, se usa CKAsset:

// Guardar una imagen
let urlTemporal = FileManager.default.temporaryDirectory
    .appendingPathComponent(UUID().uuidString + ".jpg")
try imagenData.write(to: urlTemporal)

let asset = CKAsset(fileURL: urlTemporal)
nota["imagen"] = asset

bbddPrivada.save(nota) { _, _ in
    try? FileManager.default.removeItem(at: urlTemporal)
}

// Leer la imagen
if let asset = record["imagen"] as? CKAsset,
   let url = asset.fileURL,
   let data = try? Data(contentsOf: url) {
    let imagen = UIImage(data: data)
}

Resumen

CloudKit elimina la necesidad de un servidor propio para sincronizar datos entre dispositivos Apple. El stack básico se compone de CKContainer, las tres CKDatabase con diferente visibilidad, CKRecord para los datos y CKQuery para las consultas. Para proyectos nuevos, NSPersistentCloudKitContainer hace la sincronización completamente transparente. Las suscripciones push permiten actualizaciones en tiempo real sin polling, y CKAsset gestiona ficheros adjuntos de cualquier tamaño.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP