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.
