Saltar al contenido principal
Volver al blogCopiar Markdown
·6 min de lectura
·swiftiosswiftdatacloudkitarchitecture

SwiftData y CloudKit: cómo condicionan tu modelo de datos

SwiftData con CloudKit cambia el modelo antes del sync: defaults, relaciones opcionales, sin @Attribute(.unique) y una validación que puedes copiar. →

Óscar Gallego

Ingeniero de software · Málaga

App Rydger mostrando costes de uso y mantenimiento de vehículos
En este artículo

El primer modelo de SwiftData que escribí para Rydger todavía lleva este comentario en la cabecera: Fields are CloudKit-ready: every attribute has a default value or is optional, so [a later ticket] can switch on CloudKit sync with no schema change.

Rydger es mi primera app nativa para iOS y mi primer proyecto serio con Swift. Había trabajado con React Native en una empresa, así que los problemas de estado y ciclo de vida en móvil no me pillaban de nuevas. Dejar que SwiftData replique el almacenamiento local con CloudKit sí.

Escribí ese comentario antes de que la app sincronizase nada. El container iCloud.com.rydger.Rydger ya existía, pero ModelConfiguration aún no lo usaba. El comentario me obligó a revisar el esquema pronto; no demostraba que el sync funcionase.

CloudKit cambia el modelo antes de sincronizar nada

Si vas a usar la integración automática de SwiftData con CloudKit, revisa estas cinco restricciones antes de activar cloudKitDatabase: .automatic:

  1. Cada atributo necesita un valor por defecto o tiene que ser opcional. Usa var make: String = "" o String?, no una propiedad obligatoria sin default.
  2. @Attribute(.unique) no está soportado. Es una limitación de la integración de SwiftData con CloudKit, no significa que los registros de CloudKit no tengan identidad.
  3. Las relaciones tienen que ser opcionales y tener una relación inversa. SwiftData puede inferir algunas; declara @Relationship(inverse:) cuando no pueda hacerlo bien.
  4. La regla de borrado .deny no está soportada. Usa una regla compatible y aplica cualquier restricción extra en tu código.
  5. El esquema de CloudKit en producción es aditivo. Una vez publicado, no puedes borrar sin más los tipos y campos existentes ni cambiar sus tipos. Trata esos cambios como decisiones de migración.

Un modelo incompatible compila. El fallo útil aparece al cargar el almacenamiento persistente conectado a CloudKit, muchas veces como el error 134060 de NSCocoaErrorDomain. Si añades la configuración de CloudKit justo antes de publicar, ese feedback llega demasiado tarde.

Este es el patrón reducido a lo importante:

@Model final class Vehicle {
    var make: String = ""
    @Relationship(deleteRule: .cascade, inverse: \MaintenanceItem.vehicle)
    var maintenanceItems: [MaintenanceItem]? = []

    init(make: String = "") {
        self.make = make
    }
}

@Model final class MaintenanceItem {
    var title: String = ""
    var vehicle: Vehicle?

    init(title: String = "", vehicle: Vehicle? = nil) {
        self.title = title
        self.vehicle = vehicle
    }
}

struct VehicleInput {
    let make: String
}

enum VehicleMappingError: Error {
    case missingMake
}

extension Vehicle {
    func domainInput() throws -> VehicleInput {
        let value = make.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !value.isEmpty else { throw VehicleMappingError.missingMake }
        return VehicleInput(make: value)
    }
}

La anotación exacta depende de lo que SwiftData pueda inferir al mirar ambos lados. Comprueba que las relaciones sean opcionales, que tengan una relación inversa válida y que no haya ningún .deny escondido en el modelo.

El default de persistencia no es el default del negocio

make = "" cumple el esquema de persistencia. Eso no hace que un nombre de fabricante vacío sea válido en Rydger. Si esa diferencia se queda repartida entre vistas de SwiftUI, cada pantalla nueva puede interpretarla de una forma distinta.

Rydger guarda esas decisiones en un paquete local, RydgerCore, que no importa SwiftData ni SwiftUI. La app convierte cada Vehicle en un input del dominio y rechaza o normaliza los defaults durante esa conversión. Así puedo ejecutar las funciones puras con swift test sin arrancar el simulador.

Deja que el modelo de persistencia cumpla con CloudKit y aplica la regla estricta una sola vez al convertirlo al tipo del dominio. La conversión falla de forma explícita en vez de dejar que "" recorra media app fingiendo ser un fabricante.

También tiene un coste: mantienes dos representaciones, escribes código de mapeo y alguien puede saltarse la conversión. Solo puedo hablar de una garantía de tipos más estricta en las rutas donde la app obliga a pasar por ese punto.

También guardo como String los enums que pueden crecer y hago el mapeo de forma explícita. Ese fallback pertenece al código de Rydger; CloudKit no lo pone gratis. Si llega un valor desconocido, lo convierto en .unknown o devuelvo un error de validación. No debería acabar sin avisar en el caso que venía bien cuando escribiste el modelo.

La parte honesta: cumplir el schema no resuelve el sync

Quitar .unique deja un agujero real. SwiftData mantiene la identidad de cada objeto persistente, pero no impide que dos dispositivos sin conexión creen dos registros para el mismo mantenimiento. La integración con CloudKit no se va a inventar esa regla de negocio.

Rydger no tiene una solución universal para deduplicar, y no voy a fingir lo contrario. Una huella determinista puede ayudar a detectar registros equivalentes después de leerlos, pero la integración automática no puede imponer esa huella como restricción única global. Para garantizarlo necesitas reconciliación explícita en la app u otra arquitectura con CloudKit directo o un backend.

Con la observación pasa algo parecido. @Query puede mantener una vista al día con los cambios del almacenamiento local; no explica errores de CloudKit, no resuelve duplicados de negocio, no gestiona cambios de cuenta de iCloud ni cuenta al usuario por qué aún no han llegado los cambios del otro dispositivo.

Antes de que el modelo de SwiftData se estabilice, arranca la app con el ModelContainer real conectado a CloudKit. Eso detecta incompatibilidades del esquema como el error 134060; no demuestra que el sync funcione. Después crea, edita y borra un registro en dos dispositivos con la misma cuenta de iCloud. Son dos comprobaciones distintas, y la primera es lo bastante barata para hacerla hoy.

Fuentes

Lectura relacionada: Qué es harness engineering y por qué falla tu agente, otro caso para aplicar las garantías en un punto que puedas comprobar.

Caso práctico: el caso de Rydger reúne las decisiones y lo que salió mal en la app.


P.D. Esta es mi primera app nativa para iOS. Si te has encontrado con un problema de CloudKit que aún no haya visto, dímelo en LinkedIn.

Comparte este artículo

Artículos relacionados