Posts

CloudKit: qué es y cómo funciona el backend de Apple

Arturo Rivas Arias

Cuando una aplicación necesita que sus datos estén disponibles en el iPhone, el iPad y el Mac del usuario, normalmente aparece una pieza que complica bastante la arquitectura: el backend. Hay que elegir una base de datos, crear una API, autenticar usuarios, controlar permisos, resolver conflictos, gestionar la sincronización y mantener una infraestructura que siga funcionando cuando la aplicación crezca.

CloudKit es la alternativa de Apple para una parte importante de estos casos. Es un servicio integrado con iCloud que permite almacenar datos estructurados, sincronizarlos entre dispositivos y compartirlos entre usuarios sin tener que construir y operar un servidor tradicional.

No es simplemente una base de datos remota. CloudKit incluye almacenamiento, autenticación basada en la cuenta de Apple, sincronización, zonas de almacenamiento diferenciadas, suscripciones, compartición de entidades, telemetría y herramientas para gestionar el esquema y los datos en producción. Además, puede utilizarse directamente desde el framework CloudKit o de forma indirecta a través de SwiftData y Core Data.

Para una aplicación centrada en las plataformas de Apple puede ser una solución especialmente atractiva, aunque conviene conocer bien su modelo antes de asumir que puede sustituir a cualquier backend.

Qué problema resuelve CloudKit

Una aplicación puede almacenar toda su información localmente, pero eso limita los datos al dispositivo donde fueron creados. Si el usuario cambia de iPhone, instala la aplicación en un iPad o quiere continuar trabajando desde su Mac, tendremos que proporcionar algún mecanismo para transportar esa información.

La solución tradicional suele implicar:

  • Un servidor.
  • Una base de datos.
  • Una API.
  • Un sistema de autenticación.
  • Control de permisos.
  • Gestión de errores y conflictos.
  • Monitorización.
  • Escalado de la infraestructura.

CloudKit elimina buena parte de esa infraestructura porque Apple opera los servidores y utiliza la cuenta de iCloud del usuario como identidad para los datos privados.

Esto no significa que desaparezca toda la complejidad. Seguimos teniendo que diseñar el modelo, decidir cómo se sincroniza, manejar errores, resolver conflictos y preparar las migraciones del esquema. La diferencia es que ya no somos responsables de mantener la infraestructura ni de la mayor parte del código que hay debajo.

El contenedor de CloudKit

La entidad de nivel superior en CloudKit es CKContainer.

Un contenedor aísla los datos y la configuración de una aplicación respecto a los de otras aplicaciones. Normalmente, un proyecto utiliza un único contenedor, aunque también es posible compartirlo entre varias aplicaciones relacionadas.

Podemos obtener el contenedor configurado por defecto de esta forma:

import CloudKit

let container = CKContainer.default()

O acceder explícitamente a uno concreto:

let container = CKContainer(
    identifier: "iCloud.com.example.PlantJournal"
)

Dentro de cada contenedor encontramos tres bases de datos con propósitos diferentes: privada, compartida y pública.

Base de datos privada

La base privada pertenece al usuario actual.

let database = CKContainer.default().privateCloudDatabase

Es el lugar adecuado para almacenar contenido que solo debería poder consultar ese usuario: preferencias, documentos personales, progreso, colecciones privadas o cualquier otra información vinculada a su cuenta.

Cada usuario tiene su propia base privada dentro del mismo contenedor. Los registros de una persona no son visibles para el resto de usuarios ni para el desarrollador desde CloudKit Console.

Además, los datos privados consumen almacenamiento de la cuenta de iCloud del usuario, no el almacenamiento público asignado a la aplicación.

Este modelo permite una experiencia especialmente natural en el ecosistema Apple: el usuario inicia sesión en iCloud una vez y sus datos pueden aparecer en todos sus dispositivos sin tener que crear otra cuenta específica para la aplicación.

Naturalmente, nuestra aplicación tiene que manejar el caso contrario. Si el usuario no tiene una cuenta de iCloud disponible, las operaciones sobre la base privada fallarán.

Podemos comprobar el estado de la cuenta mediante el contenedor:

let container = CKContainer.default()
let status = try await container.accountStatus()

switch status {
case .available:
    print("iCloud disponible")
case .noAccount:
    print("No hay una cuenta de iCloud")
case .restricted:
    print("Acceso restringido")
case .couldNotDetermine, .temporarilyUnavailable:
    print("Estado de iCloud no disponible")
@unknown default:
    break
}

Base de datos compartida

La base compartida contiene los registros que otros usuarios han compartido con la persona actual.

let database = CKContainer.default().sharedCloudDatabase

Aquí hay un detalle importante: compartir un registro no significa moverlo a una especie de base global. El propietario mantiene el contenido y crea un CKShare que determina qué usuarios pueden acceder a él y con qué permisos.

CloudKit distingue entre propietario y participantes. Los participantes pueden recibir permisos de solo lectura o de lectura y escritura, y el propietario puede modificar esos permisos o retirar el acceso posteriormente.

Una aplicación para catalogar plantas, por ejemplo, podría permitir compartir una colección con otra persona para que ambos usuarios pudieran añadir observaciones. Los registros originales seguirían perteneciendo al propietario, mientras que la otra persona accedería a ellos desde su base compartida.

CloudKit ofrece APIs específicas para este modelo y Apple proporciona interfaces del sistema para gestionar invitaciones y participantes, evitando que tengamos que implementar toda la experiencia desde cero.

Base de datos pública

La base pública contiene información asociada a la aplicación y potencialmente accesible para todos sus usuarios.

let database = CKContainer.default().publicCloudDatabase

Por defecto, sus registros son legibles públicamente y solo puede modificarlos su propietario, aunque los permisos pueden configurarse mediante roles en CloudKit Console.

A diferencia de las bases privada y compartida, la lectura de la base pública puede funcionar aunque el dispositivo no tenga una cuenta activa de iCloud. Para realizar escrituras, CloudKit sí necesita identificar al usuario.

Podría utilizarse, por ejemplo, para almacenar un catálogo común de especies de plantas, una lista de contenidos editoriales o cualquier otra información que deba ser igual para todos los usuarios.

La base pública nunca debería utilizarse para información que necesite privacidad.

CKRecord: la unidad básica de almacenamiento

CloudKit almacena la información mediante registros. CKRecord representa una colección de pares clave-valor perteneciente a un tipo determinado.

Por ejemplo:

import CloudKit

let record = CKRecord(recordType: "Plant")

record["name"] = "Monstera" as CKRecordValue
record["room"] = "Salón" as CKRecordValue
record["lastWatered"] = Date() as CKRecordValue
record["needsSunlight"] = true as CKRecordValue

Plant sería el record type y cada instancia concreta sería un registro.

Los campos admiten tipos como cadenas, números, fechas, datos binarios, ubicaciones, referencias y otros valores compatibles con CloudKit.

Una vez creado el registro podemos guardarlo utilizando las APIs de concurrencia de Swift:

let database = CKContainer.default().privateCloudDatabase

do {
    let savedRecord = try await database.save(record)
    print("Registro guardado: \(savedRecord.recordID)")
} catch {
    print("No se pudo guardar el registro: \(error)")
}

Y recuperarlo posteriormente mediante su identificador:

let recordID = CKRecord.ID(recordName: "monstera-living-room")
let plant = try await database.record(for: recordID)

Los registros recuerdan a los objetos de un modelo, pero CloudKit no sustituye al modelo de datos de nuestra aplicación. Apple lo define principalmente como un mecanismo para mover información entre la aplicación e iCloud.

Esta diferencia es fundamental para diseñar correctamente la persistencia.

CloudKit no es una base de datos local

El framework CloudKit ofrece una capacidad de caché local limitada, pero no mantiene automáticamente una réplica completa de los datos.

Si construimos una aplicación directamente sobre CKRecord y queremos que funcione bien sin conexión, tendremos que mantener nuestro propio almacenamiento local y sincronizarlo con CloudKit.

Ese almacenamiento puede implementarse con SwiftData, Core Data, SQLite u otra solución. El patrón habitual es que la interfaz trabaje siempre contra los datos locales mientras una capa de sincronización intercambia cambios con iCloud.

De esta forma la aplicación puede arrancar, mostrar contenido y aceptar modificaciones aunque temporalmente no exista conexión de red.

Precisamente por eso SwiftData y Core Data resultan tan interesantes: ambos pueden encargarse de mantener la copia local mientras CloudKit actúa como mecanismo de sincronización.

Zonas y sincronización eficiente

Una CKRecordZone es una partición de una base de datos que agrupa registros relacionados.

let database = CKContainer.default().privateCloudDatabase
let zone = CKRecordZone(zoneName: "Greenhouse")

try await database.save(zone)

Después podemos crear registros dentro de esa zona:

let zoneID = CKRecordZone.ID(
    zoneName: "Greenhouse",
    ownerName: CKCurrentUserDefaultName
)

let recordID = CKRecord.ID(
    recordName: UUID().uuidString,
    zoneID: zoneID
)

let sensor = CKRecord(
    recordType: "SensorReading",
    recordID: recordID
)

sensor["humidity"] = 61.0 as CKRecordValue
sensor["temperature"] = 24.3 as CKRecordValue

Las zonas no son simplemente una forma “estética” de ordenar datos. CloudKit mantiene historial de cambios por zonas y permite consultar únicamente qué registros han cambiado desde un punto determinado.

Esto evita tener que descargar periódicamente toda la base de datos para comprobar si existe alguna modificación.

También tienen un papel importante en la compartición. Core Data, por ejemplo, utiliza zone sharing para mover el grafo de objetos que se comparte a una zona propia.

CKSyncEngine: sincronización directa sin reinventarlo todo

Cuando usamos CloudKit directamente y mantenemos nuestra propia base local, uno de los problemas más complejos es construir correctamente el motor de sincronización: saber qué cambios locales quedan por enviar, obtener solo los cambios remotos pendientes, reaccionar a notificaciones, persistir tokens y reintentar operaciones transitorias.

Apple ofrece CKSyncEngine precisamente para cubrir esta capa.

final class SyncCoordinator: CKSyncEngineDelegate {
    private var syncEngine: CKSyncEngine!

    func start(
        persistedState: CKSyncEngine.State.Serialization?
    ) {
        let database = CKContainer.default().privateCloudDatabase

        let configuration = CKSyncEngine.Configuration(
            database: database,
            stateSerialization: persistedState,
            delegate: self
        )

        syncEngine = CKSyncEngine(configuration)
    }

    // Implementación de los métodos del delegate...
}

El motor puede enviar y recibir cambios automáticamente cuando el sistema considera que existen buenas condiciones de red, batería y disponibilidad de la cuenta.

Además, crea (o reutiliza) una suscripción de CloudKit para recibir notificaciones silenciosas cuando existen cambios en el servidor y gestiona por su cuenta determinados errores transitorios, como falta temporal de red, limitaciones de peticiones o la falta de disponibilidad momentánea del servicio.

Sin embargo, no elimina toda la responsabilidad de la aplicación.

Tenemos que informar al motor de los cambios locales pendientes, proporcionar los CKRecord que debe enviar, procesar los registros recibidos y, especialmente, persistir el estado serializado que CKSyncEngine entrega mediante sus eventos.

Ese estado contiene información imprescindible para continuar la sincronización de forma eficiente después de cerrar y volver a abrir la aplicación.

CKSyncEngine puede trabajar con las bases privada y compartida, pero Apple indica explícitamente que no debe utilizarse para sincronizar la base pública.

Además, la sincronización automática no tiene un momento exacto garantizado. Si nuestra aplicación necesita obtener los últimos cambios antes de realizar una operación concreta, podemos solicitar una sincronización manual mediante fetchChanges y sendChanges.

Suscripciones y notificaciones

Antes de CKSyncEngine, una integración directa con CloudKit normalmente requería crear suscripciones mediante CKSubscription o alguno de sus subtipos.

Las suscripciones permiten indicar al servidor que queremos recibir una notificación cuando cambien determinados datos. Así evitamos consultar continuamente CloudKit para descubrir si hay algo nuevo.

El patrón es especialmente importante en aplicaciones que mantienen una base local:

  1. La aplicación trabaja contra los datos locales.
  2. Se produce un cambio en otro dispositivo.
  3. CloudKit envía una notificación silenciosa.
  4. La aplicación obtiene únicamente los cambios pendientes.
  5. La base local se actualiza.
  6. La interfaz refleja el nuevo estado.

CKSyncEngine automatiza buena parte de este proceso, incluyendo la gestión de la suscripción que utiliza para conocer los cambios remotos.

SwiftData con CloudKit

Para muchos proyectos actuales, la forma más sencilla de obtener sincronización privada no consiste en trabajar directamente con CKRecord, sino en utilizar SwiftData.

Un modelo podría ser:

import SwiftData

@Model
final class Plant {
    var name: String = ""
    var room: String = ""
    var lastWatered: Date?

    init(
        name: String,
        room: String,
        lastWatered: Date? = nil
    ) {
        self.name = name
        self.room = room
        self.lastWatered = lastWatered
    }
}

Y podemos asociar el ModelContainer a una base privada de CloudKit:

let configuration = ModelConfiguration(
    cloudKitDatabase: .private(
        "iCloud.com.example.PlantJournal"
    )
)

let container = try ModelContainer(
    for: Plant.self,
    configurations: configuration
)

A partir de ahí seguimos trabajando con SwiftData de forma local. El framework utiliza internamente NSPersistentCloudKitContainer para convertir los cambios en registros de CloudKit y sincronizarlos entre dispositivos.

Para que la sincronización automática funcione hay que habilitar la capacidad de iCloud con CloudKit y también Remote notifications dentro de Background Modes. SwiftData utiliza esas notificaciones silenciosas para conocer los cambios que llegan desde otros dispositivos.

El modelo, además, tiene que ser compatible con las restricciones de CloudKit. Algunas características de SwiftData no pueden representarse directamente, como las restricciones @Attribute(.unique) y las relaciones no opcionales.

Este detalle es fácil de pasar por alto porque un modelo perfectamente válido para una base SwiftData local puede dejar de serlo en cuanto intentamos sincronizarlo mediante CloudKit.

Y existe otra limitación importante: la sincronización automática de SwiftData está pensada para los datos privados del usuario y actualmente no proporciona soporte para CloudKit Sharing. Si necesitamos colaboración entre varias cuentas de iCloud tendremos que utilizar Core Data con CloudKit o trabajar directamente con el framework CloudKit.

Core Data con CloudKit

Core Data dispone de una integración más madura mediante NSPersistentCloudKitContainer.

let container = NSPersistentCloudKitContainer(
    name: "PlantModel"
)

container.loadPersistentStores { _, error in
    if let error {
        print("Error cargando el store: \(error)")
    }
}

El contenedor mantiene un store local de Core Data y refleja sus cambios hacia CloudKit.

Además de la sincronización privada, NSPersistentCloudKitContainer ofrece APIs para trabajar con la base compartida, aceptar invitaciones y crear CKShare asociados a objetos gestionados.

Por eso Core Data sigue teniendo una ventaja clara frente a SwiftData en aplicaciones colaborativas. No es necesario abandonar el modelo local de Core Data para implementar CloudKit Sharing.

SwiftData, Core Data o CloudKit directo

Las tres alternativas utilizan la misma infraestructura, pero ofrecen niveles de abstracción muy diferentes.

SwiftData + CloudKit es la opción más sencilla si necesitamos persistencia local y sincronización privada entre los dispositivos del mismo usuario. Es la alternativa con menos código, aunque también la que ofrece menos control sobre el proceso.

Core Data + CloudKit resulta especialmente útil para aplicaciones existentes basadas en Core Data o cuando necesitamos sincronización local junto con compartición entre usuarios.

CloudKit directo ofrece control sobre registros, zonas, consultas, suscripciones, compartición y resolución de conflictos. Es la opción más flexible, pero también implica diseñar nuestra propia persistencia local y decidir con precisión cómo se coordinan ambos lados.

CKSyncEngine reduce bastante esa complejidad en los proyectos que eligen la tercera alternativa.

El esquema y los entornos de desarrollo y producción

CloudKit distingue entre un entorno de desarrollo y otro de producción.

Durante el desarrollo, el esquema es flexible. Si guardamos un nuevo tipo de registro o añadimos un campo, CloudKit puede incorporarlo automáticamente al esquema.

Eso permite iterar con rapidez:

let record = CKRecord(recordType: "WateringEvent")
record["date"] = Date() as CKRecordValue
record["amount"] = 250 as CKRecordValue

Si WateringEvent todavía no existe en el entorno de desarrollo, CloudKit puede crear el tipo y sus campos a partir de ese registro.

Antes de publicar la aplicación tenemos que desplegar el esquema a producción desde CloudKit Console.

Y aquí aparece una de las restricciones más importantes de CloudKit: una vez que un tipo o un campo forma parte del esquema de producción, no podemos eliminarlo. Las evoluciones posteriores son fundamentalmente aditivas.

Podemos crear nuevos tipos y añadir nuevos campos, pero una migración que en una base local consistiría simplemente en renombrar o eliminar una propiedad requiere mucha más planificación en CloudKit.

Por tanto, el esquema remoto debe considerarse una API persistente que puede seguir siendo utilizada por versiones antiguas de la aplicación.

CloudKit Console

CloudKit Console es la herramienta web con la que Apple permite gestionar los contenedores.

Desde ella podemos:

  • Inspeccionar el esquema.
  • Crear tipos de registro y campos.
  • Configurar índices.
  • Consultar y editar datos de desarrollo.
  • Desplegar cambios del esquema a producción.
  • Revisar telemetría.
  • Consultar logs del servidor.
  • Configurar alertas.

La separación entre desarrollo y producción es especialmente útil para experimentar sin modificar los datos reales de los usuarios.

Antes de lanzar una aplicación conviene probar expresamente contra producción. Un proyecto puede funcionar perfectamente durante meses en desarrollo y fallar en TestFlight si olvidamos desplegar un nuevo tipo de registro o un nuevo campo.

Privacidad y cifrado

CloudKit protege los datos durante el transporte y permite marcar determinados campos de los registros privados para utilizar cifrado adicional.

Desde código podemos trabajar con esos campos mediante encryptedValues:

let record = CKRecord(recordType: "PlantNote")

record["title"] = "Observación semanal" as CKRecordValue
record.encryptedValues["privateNote"] =
    "Revisar tratamiento la próxima semana" as CKRecordValue

Los campos cifrados tienen una contrapartida: el servidor no puede leer su contenido y, por tanto, no pueden utilizarse como campos indexados para realizar consultas.

También hay que tener en cuenta que un campo existente sin cifrar no puede convertirse posteriormente en un campo cifrado dentro del esquema de producción. Hay que introducir un nuevo campo.

La privacidad del modelo de CloudKit va más allá del cifrado. El desarrollador no puede inspeccionar en CloudKit Console los registros privados de sus usuarios como si fueran filas de una base de datos convencional.

Cuánto cuesta CloudKit

CloudKit forma parte de las capacidades disponibles para aplicaciones distribuidas mediante el Apple Developer Program.

Los datos privados escalan utilizando el almacenamiento de iCloud de cada usuario. El crecimiento de la base privada no consume una única bolsa de almacenamiento perteneciente al desarrollador.

Para los datos públicos, Apple anuncia actualmente hasta 1 PB de almacenamiento por aplicación.

Eso no debe interpretarse como infraestructura ilimitada. CloudKit aplica límites de almacenamiento, transferencia y operaciones, y una aplicación debe responder correctamente a errores de cuota, limitaciones temporales y políticas de reintento.

En particular, guardar grandes cantidades de información pública únicamente porque el almacenamiento parece gratuito puede convertirse rápidamente en una mala decisión de arquitectura.

Las limitaciones de CloudKit

CloudKit encaja muy bien en determinadas aplicaciones, pero su integración tan profunda con el ecosistema Apple también marca sus límites.

La primera limitación es evidente: las mejores APIs están diseñadas para las plataformas de Apple. Existe CloudKit JS y también una API web, pero no hay un SDK nativo equivalente para construir una aplicación Android con la misma integración.

La segunda es la dependencia de iCloud. Si nuestra funcionalidad principal necesita datos privados y el usuario no dispone de una cuenta de iCloud operativa, tendremos que definir qué experiencia ofrecer.

También tenemos menos control sobre la infraestructura. CloudKit no está pensado para ejecutar lógica compleja de servidor, crear procesos arbitrarios en segundo plano o sustituir servicios especializados de búsqueda, procesamiento o analítica.

Por último, la sincronización no es instantánea por definición. Tanto SwiftData como Core Data y CKSyncEngine colaboran con el sistema para decidir cuándo resulta apropiado transferir cambios. Podemos solicitar sincronizaciones explícitas en algunos escenarios, pero no debemos diseñar la interfaz suponiendo que cada modificación llegará inmediatamente a todos los dispositivos.

Cuándo elegir CloudKit

CloudKit es especialmente interesante cuando:

  • La aplicación está centrada en iOS, iPadOS, macOS, watchOS o visionOS.
  • Los datos pertenecen principalmente a usuarios de iCloud.
  • Queremos sincronización entre dispositivos sin mantener servidores propios.
  • Necesitamos compartir determinados datos entre usuarios.
  • El modelo encaja bien en registros, campos, relaciones y zonas.
  • Podemos aceptar que Apple controle la infraestructura y parte de la planificación de la sincronización.

En cambio, puede no ser la mejor opción si Android y web tienen el mismo peso que las plataformas de Apple, si necesitamos autenticación completamente independiente de iCloud, si la aplicación depende de mucha lógica de servidor o si necesitamos control absoluto sobre la base de datos y la infraestructura.

También es perfectamente válido combinarlo con otros servicios. Una aplicación puede utilizar CloudKit para sincronizar información privada entre dispositivos y recurrir a un backend convencional para otras funciones.

Conclusión

CloudKit es mucho más que una base de datos remota. Es la infraestructura de Apple para almacenar, sincronizar y compartir datos utilizando iCloud, con un modelo especialmente cómodo para aplicaciones nativas del ecosistema.

La decisión más importante no consiste únicamente en saber si queremos utilizar CloudKit, sino en qué nivel queremos utilizarlo.

SwiftData proporciona la ruta más sencilla para sincronizar automáticamente datos privados. Core Data añade una integración madura y soporte para compartir objetos entre usuarios. El framework CloudKit ofrece el máximo control, y CKSyncEngine permite construir una sincronización personalizada sin tener que implementar desde cero toda la maquinaria de cambios, suscripciones y reintentos.

Entender esas diferencias permite elegir una arquitectura que aproveche CloudKit por lo que realmente ofrece, sin convertir una herramienta pensada para simplificar la infraestructura en una nueva fuente de complejidad.