← Todos los articulos

El verdadero costo de SwiftData es la disciplina de esquema

El ShoppingItem de Get Bananas es el ejemplo canónico de por qué importa la disciplina de esquema en SwiftData. El esquema original no incluía una marca de tiempo lastModified; agregarla más tarde requirió una forma de migración específica porque ya había datos existentes en disco, y el campo se hizo opcional precisamente para corregir un fallo de migración que apareció cuando se agregó por primera vez como no opcional.1

La API de SwiftData son dos macros. @Model sobre una clase la convierte en un tipo persistente. @Attribute(.unique) sobre una propiedad le otorga una restricción de unicidad. El framework oculta la gestión del stack de Core Data, el baile del value-transformer y el código repetitivo de NSManagedObjectContext. Lo que el framework no oculta es la migración de esquema; simplemente la hace declarativa en lugar de imperativa. El costo de no prestar atención a las migraciones es el bug que borra los datos de un usuario en una actualización rutinaria.

La tesis: SwiftData es barato de empezar y caro de migrar de forma descuidada. La disciplina está en el nombrado, la opcionalidad y VersionedSchema desde el primer día, no el día en que te das cuenta de que deberías haberlo hecho.

TL;DR

  • La macro @Model convierte una clase en un tipo persistente de SwiftData. El framework genera el esquema a partir de las declaraciones de propiedades en tiempo de compilación.
  • Agregar una nueva propiedad opcional es una migración nula: la migración ligera de SwiftData la maneja. Agregar una propiedad no opcional a un esquema existente requiere un VersionedSchema más un MigrationPlan que le indica al framework cómo poblar el nuevo campo para las filas existentes.
  • El costo de saltarte VersionedSchema desde el primer día es que cualquier cambio de esquema v2 no trivial arriesga eliminar la base de datos de un usuario, porque la ruta ligera es conservadora y se detiene cuando no puede inferir la migración.
  • @Attribute(.unique) es la herramienta correcta para claves naturales (un UUID que generaste, un ID externo que importaste). @Relationship es la herramienta correcta para referencias padre/hijo. Ambas son macros que generan la plomería correcta de Core Data por debajo.2

Lo que @Model realmente hace

Un tipo de SwiftData es una clase de Swift con la macro @Model aplicada. El ShoppingItem de Get Bananas es la forma canónica:

import Foundation
import SwiftData

@Model
final class ShoppingItem {
    @Attribute(.unique) var id: UUID
    var name: String
    var amount: String
    var section: String
    var isChecked: Bool
    var isOptional: Bool
    var sortOrder: Int
    var lastModified: Date?

    init(id: UUID = UUID(), name: String, amount: String, section: String,
         isOptional: Bool = false, sortOrder: Int = 0) {
        self.id = id
        self.name = name
        self.amount = amount
        self.section = section
        self.isChecked = false
        self.isOptional = isOptional
        self.sortOrder = sortOrder
        self.lastModified = Date()
    }
}

Tres detalles sobre esa forma que la API oculta.

@Model no requiere una declaración de esquema de almacén persistente por separado. SwiftData lee la definición de la clase en tiempo de compilación y sintetiza el esquema. Las propiedades de la clase se convierten en los atributos del modelo; sus tipos de Swift se convierten en los tipos de columna. No hay un archivo .xcdatamodeld que mantener (aunque el NSManagedObjectModel subyacente de Core Data sigue existiendo y es lo que respalda el esquema en tiempo de ejecución).2

@Attribute(.unique) es una restricción sobre una sola columna, no una declaración de PRIMARY KEY. La identidad persistente de SwiftData es el PersistentIdentifier, generado automáticamente por fila. La declaración @Attribute(.unique) le dice al framework “esta columna almacena como máximo una fila por valor”. Cuando insertas un modelo con un valor .unique que ya existe, SwiftData realiza un upsert: la fila existente se actualiza en lugar de rechazarse. La semántica importa para el código de producto: .unique no es una validación a nivel de UI que impida que se envíen duplicados; es una garantía de almacenamiento de como máximo uno que fusiona silenciosamente. El patrón id: UUID de arriba es el recomendado para la sincronización entre procesos (donde quieres un identificador estable que sobreviva a la desaparición del PersistentIdentifier en proceso), y el comportamiento de upsert es exactamente lo que quieres cuando el mismo UUID llega desde dos rutas de sincronización.

Las clases @Model son tipos por referencia, no tipos por valor. Mutar una propiedad en una instancia de ShoppingItem activa el seguimiento de cambios de SwiftData; el framework registra el cambio y lo persiste en el siguiente guardado del contexto. La integración con SwiftUI a través de @Query vuelve a renderizar cualquier vista que observe el predicado coincidente. El patrón es similar a @Observable (cubierto en De qué está hecho SwiftUI), con la persistencia montada encima.

Los campos opcionales son la migración barata

El campo lastModified: Date? en ShoppingItem es opcional, y la opcionalidad es estructural. El campo se agregó después de publicar la v1 para dar soporte a la sincronización entre dispositivos y a la resolución de conflictos; las filas existentes en los dispositivos de los usuarios no tenían valor de lastModified. Un campo opcional sin valor por defecto permite que la migración ligera de SwiftData maneje la adición sin escribir ningún código de migración: las filas existentes obtienen nil; las filas nuevas obtienen lo que sea que el init establezca.3

La ruta de migración ligera es la ruta cortés del framework. SwiftData inspecciona el nuevo esquema y el almacén persistente, infiere el cambio compatible más pequeño y lo aplica. La migración es automática; el usuario no ve nada; la app se inicia normalmente sobre los datos existentes. Los casos que la ruta ligera maneja limpiamente:

  • Agregar una propiedad opcional
  • Eliminar una propiedad (los datos se descartan; las lecturas existentes ya no ven la columna)
  • Renombrar un atributo que el framework puede emparejar mediante una pista (usando @Attribute(originalName: ...))
  • Renombrar una clase @Model que el framework puede emparejar (usando @Model.originalName o una pista)

Los casos en los que la ruta ligera se detiene:

  • Agregar una propiedad no opcional sin valor por defecto a un esquema existente (las filas existentes no tienen valor con el cual poblarla)
  • Cambiar el tipo de una propiedad (por ejemplo, IntString)
  • Dividir un modelo en dos modelos, o fusionar dos en uno
  • Cualquier cosa que requiera lógica personalizada para migrar

Cuando la ruta ligera se detiene, el comportamiento seguro es fallar la migración. El comportamiento inseguro sería eliminar la base de datos y empezar de cero; el framework es conservador y se niega a hacer eso silenciosamente. El usuario ve que la app falla al iniciarse con un error de migración; el desarrollador ve un stack trace que apunta a la discrepancia de esquema; nadie pierde datos, pero todos pierden confianza.

El costo de saltarte VersionedSchema desde el primer día aparece en la frontera v2 → v3, cuando agregas la tercera función cuyo cambio de esquema excede lo que la ruta ligera maneja.

VersionedSchema y MigrationPlan: la disciplina del primer día

VersionedSchema declara una versión específica del esquema del modelo. MigrationPlan declara cómo migrar de una versión a la siguiente.4 La forma:

import SwiftData

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] = [
        SchemaV1.self,
        SchemaV2.self,
    ]

    static var stages: [MigrationStage] = [
        MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
    ]
}

Las clases del modelo en sí se mueven al espacio de nombres del esquema versionado:

extension SchemaV1 {
    @Model
    final class ShoppingItemV1 { /* v1 fields */ }
}

extension SchemaV2 {
    @Model
    final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}

El ModelContainer se construye con el plan de migración:

let container = try ModelContainer(
    for: ShoppingItemV2.self,
    migrationPlan: AppMigrationPlan.self,
    configurations: ModelConfiguration("ShoppingList")
)

El plan de migración le da al framework un grafo tipado de cómo evoluciona el esquema. Cuando la app que publica v2 se inicia contra una base de datos v1, el framework recorre el plan de migración, aplica las etapas nombradas y lleva la base de datos a v2. Cuando publicas v3, agregas SchemaV3.self a schemas y un nuevo MigrationStage entre v2 y v3.

La disciplina es publicar VersionedSchema en v1, incluso cuando solo hay una versión. El costo de hacerlo es un archivo extra y una declaración enum extra. El costo de no hacerlo es que el primer cambio de esquema no trivial de v2 requiere envolver retroactivamente la v1 en un VersionedSchema, lo cual es factible pero requiere cuidado para coincidir con la forma exacta de la v1 de modo que el framework pueda identificar los datos existentes como SchemaV1. El tú-futuro trabajando en v2 pagará el impuesto; el tú-presente puede pagarlo una sola vez y olvidarse de él.

MigrationStage personalizado para los casos difíciles

Las migraciones ligeras cubren la mayoría de los cambios aditivos. Los cambios de tipo, las divisiones, las fusiones y las poblaciones condicionales necesitan un MigrationStage.custom:

static var stages: [MigrationStage] = [
    MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            // Read v1 rows; stage any derived state to a transient store
            // (UserDefaults / temp file) since the v1 and v2 contexts do
            // not share state, and didMigrate cannot read v1.
            let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
            stageDerivedState(from: v1Items)
        },
        didMigrate: { context in
            // Populate v2-only fields on existing rows
            let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
            for item in v2Items where item.lastModified == nil {
                item.lastModified = Date()
            }
            try context.save()
        }
    )
]

Los dos closures se disparan antes y después de que el framework aplique la migración estructural. willMigrate se ejecuta contra el esquema v1; didMigrate se ejecuta contra el esquema v2. El cuerpo del closure es código normal de SwiftData (fetch descriptors, guardados de model context, las mismas API usadas en la app en ejecución), operando contra un contexto transitorio en plena migración.

El patrón que sobrevive en producción es mantener willMigrate vacío y poner toda la lógica de población en didMigrate. Leer datos de v1 dentro de willMigrate está permitido, pero el esquema v2 aún no existe desde la perspectiva del framework, por lo que cualquier cálculo tiene que ser almacenado en un almacén transitorio que el closure didMigrate pueda leer. La regla más simple: las migraciones estructurales son trabajo del framework; poblar los campos exclusivos de v2 en filas existentes es trabajo de didMigrate.

Cuándo @Attribute y @Relationship se ganan sus nombres

Dos macros hacen la mayor parte del trabajo de decoración de esquema en las clases @Model.

@Attribute decora una sola propiedad con una restricción o pista:

  • @Attribute(.unique) impone unicidad, como en ShoppingItem.id
  • @Attribute(.externalStorage) almacena grandes blobs de Data fuera de la base de datos (datos de imagen, buffers de audio)
  • @Attribute(originalName: "old_field_name") empareja una propiedad con una columna renombrada durante la migración
  • @Attribute(.transformable(by: ...)) aplica un ValueTransformer a un tipo no Codable

La disciplina correcta: usa .unique para campos que genuinamente deberían ser únicos (un UUID que generaste, un ID externo), usa .externalStorage para cualquier blob de más de unos pocos KB, usa originalName cuando un renombrado en v2 de una propiedad perdería de otro modo los datos de v1.

@Relationship decora una propiedad que apunta a otra clase @Model o a una colección de ellas:

@Model
final class List {
    var name: String

    @Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
    var items: [ShoppingItem] = []
}

@Model
final class ShoppingItem {
    var name: String
    var list: List?
}

El deleteRule: .cascade significa que eliminar el List padre elimina todas las filas ShoppingItem hijas. El parámetro inverse: le dice al framework qué propiedad del hijo apunta de vuelta al padre; el framework lo usa para un mantenimiento bidireccional predecible. SwiftData a veces puede inferir el inverso automáticamente, y se admite inverse: nil para relaciones explícitamente unidireccionales, pero el valor por defecto seguro es declarar inverse: siempre que la inferencia sea ambigua.5

La disciplina correcta: declara las relaciones con un deleteRule explícito (el valor por defecto es .nullify, que rara vez es lo que quieres) y declara inverse: siempre que la relación sea bidireccional (en lugar de confiar en la inferencia del framework). Los valores por defecto implícitos suelen estar equivocados; la forma explícita es un parámetro extra y un bug evitado para siempre.

Cruzar una frontera de actor: envía el identificador, no el grafo

Una clase @Model no es Sendable, y la jugada correcta es dejar de intentar que lo sea. La instancia es una referencia a un grafo de objetos vivo sostenido por un ModelContext; el framework no puede prometer que ese grafo sea seguro de leer desde otro actor, así que el tipo se deja deliberadamente como no Sendable. Forzar la conformidad no hace que la condición de carrera desaparezca; la oculta.7

El patrón que funciona es enviar la identidad y los valores planos, luego volver a hacer fetch en el otro lado. PersistentIdentifier es Sendable, así que cruza la frontera limpiamente. Extrae los valores escalares que el destino necesite (un nombre, un flag, un delta en un struct pequeño), pásalos junto con el identificador, y haz que el actor receptor vuelva a hacer fetch del modelo desde su propio contexto usando el identificador:

// On the source actor: extract identity + plain values, never the model.
let id: PersistentIdentifier = item.persistentModelID
let snapshot = ItemSnapshot(name: item.name, isChecked: item.isChecked)

// On the destination actor: re-fetch from this context, then mutate.
let fetched = destinationContext.model(for: id) as? ShoppingItem

El modo de fallo que hay que evitar es pasar el propio grafo del modelo. Cuando parte del grafo cruza la frontera, el receptor obtiene un modelo que se hidrata parcialmente en el lado lejano: las relaciones y las propiedades cargadas de forma perezosa que nunca se materializaron en el contexto de origen se resuelven contra el contexto equivocado (o no lo hacen en absoluto), y los bugs que siguen son del tipo silencioso. El identificador más los valores extraídos es el contrato seguro; el grafo no lo es. Un ModelActor encapsula esta disciplina al poseer un contexto y entregar valores en lugar de instancias.7

Sincronización con CloudKit y la trampa del entitlement de app group

Mover un almacén de SwiftData a un contenedor de app group para que un widget o una extension puedan leerlo interactúa con la sincronización de CloudKit de una manera que muerde a las apps después de publicarlas. Dos hechos hacen que lo demás se siga.

Primero, la ubicación del almacén. Con el ModelConfiguration por defecto, SwiftData copia el almacén existente al contenedor de app group por ti cuando una app evoluciona de sin grupo a app group; la redacción de Apple es que SwiftData “copia el almacén existente al contenedor de app group”.8 Con una URL de almacén personalizada, tú eres dueño de la ubicación: copias el archivo al nuevo contenedor y apuntas la configuración a él tú mismo. La ruta por defecto es la conveniente precisamente porque el framework hace la copia; la ruta personalizada cambia esa conveniencia por control.

Segundo, el entitlement. Cada miembro del app group que lea un almacén sincronizado con CloudKit debe llevar el mismo entitlement de CloudKit, porque cada uno de esos procesos sincronizará ese contenedor por cuenta propia. Ese requisito es la trampa: un widget o una extension no tiene el presupuesto de runtime ni la ventana de primer plano para impulsar una sincronización verbosa, y entregarle el entitlement de CloudKit lo fuerza a intentarlo. La solución es dividir en dos instancias de ModelConfiguration: un almacén sincronizado (entitlement de CloudKit, propiedad de la app principal) y un almacén local en el contenedor de app group que el widget y las extensions leen sin sincronizar nunca. Pon la sincronización donde una app en primer plano pueda hacerla bien, y mantén los datos compartidos-para-lectura fuera de la ruta de sincronización.8

Lo que construiría de otra forma

Tres patrones que las apps del clúster o bien publican o bien desearían haber publicado.

Publica VersionedSchema desde la v1. Cada clase @Model que se publique debería vivir dentro de un VersionedSchema desde el primer día. El costo es un enum envolvente por versión de esquema. El beneficio es que el primer cambio no trivial de v2 es una adición de una línea a MigrationPlan.schemas en lugar de una refactorización retroactiva de dos días.

Haz que cada marca de tiempo sea opcional. Los campos como lastModified, createdAt y updatedAt que existen para la sincronización entre dispositivos o la resolución de conflictos deberían ser opcionales en v1 si el producto v1 no los necesita. La opcionalidad mantiene barata la migración a v2 (cuando sí los necesites). Llenarlos en filas existentes durante didMigrate es un solo bucle; hacerlos no opcionales desde v1 es una restricción que puede romper el relleno retroactivo sobre los datos del usuario.

Usa UUID como la clave natural, no el PersistentIdentifier. El PersistentIdentifier de SwiftData es en proceso. La sincronización entre dispositivos, la integración con MCP (cubierta en Dos ecosistemas de agentes, una lista de compras) y cualquier referencia fuera de proceso necesitan un identificador estable. Un UUID con @Attribute(.unique) es la forma correcta; el PersistentIdentifier en proceso es la forma equivocada para cualquier cosa que cruce una frontera de proceso.

Cuándo @Model es la respuesta equivocada

Tres casos donde SwiftData no es la herramienta correcta:

Estado clave/valor de un solo registro. La configuración de la app, el idioma seleccionado del usuario, la marca de tiempo de la última sincronización. Usa UserDefaults o NSUbiquitousKeyValueStore (cubierto en Cinco plataformas de Apple, tres archivos compartidos). La sobrecarga de SwiftData para una sola fila es ceremonia desperdiciada; los almacenes clave-valor son el sustrato correcto.

Datos con autoridad en el servidor sin escrituras sin conexión. Una lista obtenida de una API REST y mostrada en solo lectura. SwiftData es excesivo si la fuente de verdad es el servidor y la caché local es solo una caché. Una simple instantánea Codable en Documents/ más un arreglo en caché en memoria es suficiente; el impuesto de migración de SwiftData no vale la pena pagarlo si los datos no sobreviven a un reinicio forzado.

Coordinación multiproceso. SwiftData opera dentro de un proceso. Un servidor MCP que se ejecuta fuera de la app de iOS no puede leer ni escribir el contenedor de SwiftData de la app. El estado entre procesos necesita una forma diferente: un archivo JSON en iCloud Drive, un contenedor de App Group compartido o una capa de sincronización explícita que tienda un puente entre procesos. (Get Bananas empareja SwiftData con JSON en iCloud Drive exactamente por esta razón.)6

Los datos son grandes blobs que cambian rara vez. Un archivo de audio de 10MB, un conjunto de imágenes de 50MB. Usa @Attribute(.externalStorage) si los blobs están dentro de filas de SwiftData; de lo contrario, usa el sistema de archivos directamente con metadatos en SwiftData que apunten a URL de archivo.

Lo que el patrón significa para las apps que se publican en iOS 26+

Tres conclusiones.

  1. Las macros son la parte fácil. Las migraciones son el costo. @Model y @Attribute son declaraciones de dos líneas que ocultan mucha plomería de Core Data. La disciplina de migración es lo que realmente pagas a lo largo de la vida de la app; diseña la v1 pensando en la v2.

  2. VersionedSchema desde el primer día es innegociable para apps que se publican. El enum envolvente es un archivo extra. El costo retroactivo de agregarlo más tarde es mucho mayor.

  3. Los campos opcionales y las relaciones explícitas son el seguro barato. Marcas de tiempo opcionales para los metadatos de sincronización, deleteRule e inverse: explícitos en las relaciones. Ambos son declaraciones diminutas que compran mucha flexibilidad para v2.

El clúster completo del Ecosistema Apple: App Intents tipados para Apple Intelligence; servidores MCP para agentes multi-LLM; la pregunta de enrutamiento entre ellos; Foundation Models para LLM en dispositivo y el protocolo Tool; Live Activities para la máquina de estados de la pantalla de bloqueo en iOS; el contrato de runtime de watchOS en el Apple Watch; los internals de SwiftUI para el sustrato del framework; el modelo mental espacial de RealityKit para las escenas de visionOS; los patrones de Liquid Glass para la capa visual; la publicación multiplataforma para el alcance entre dispositivos. El hub está en la serie del Ecosistema Apple. Para un contexto más amplio de iOS con agentes de IA, consulta la guía de desarrollo de agentes en iOS.

Preguntas frecuentes

¿Cuál es la diferencia entre @Model y el NSManagedObject de Core Data?

@Model es una macro de Swift que genera la plomería de NSManagedObject por debajo. SwiftData usa Core Data como su almacén de respaldo, así que el modelo en tiempo de ejecución es el mismo; la diferencia es la superficie. @Model elimina el archivo .xcdatamodeld, la ceremonia del value-transformer y la gestión del ciclo de vida de NSManagedObjectContext. Obtienes el mismo almacén persistente con una API de forma Swift.

¿Necesito VersionedSchema si nunca planeo cambiar el esquema?

Si tu app podría publicar una v2, sí. Si es una demo de una sola vez, no. El costo de VersionedSchema desde la v1 es una declaración enum extra. El costo de agregarlo retroactivamente en la v2 es coincidir con la forma exacta del esquema v1 para que el framework reconozca los datos existentes, lo cual es factible pero propenso a errores. La mayoría de las apps que se publican eventualmente necesitarán un cambio de esquema; presupuéstalo en la v1.

¿Cuándo debería usar @Attribute(.unique)?

Cuando el campo es una clave natural para la fila: un UUID que generaste, un ID externo que importaste, un slug que asignaste. SwiftData trata .unique como upsert: si insertas un modelo cuyo valor .unique ya existe, la fila existente se actualiza en lugar de agregar una nueva fila. Esa semántica es lo que hace seguras las rutas de sincronización estilo upsert (el mismo UUID llegando desde dos dispositivos); también es por lo que .unique es la herramienta equivocada en campos de nombre visible como title, porque dos usuarios escribiendo el mismo título fusionarían silenciosamente sus filas en lugar de producir dos registros distintos.

¿Cómo manejo un campo no opcional agregado a un esquema existente?

Usa un MigrationStage.custom con un closure didMigrate que pueble el campo en las filas existentes. O, más fácil: declara el campo como opcional en la nueva versión del esquema y llénalo de forma perezosa al acceder. La opcionalidad es la migración más barata; las adiciones no opcionales necesitan lógica de población explícita.

¿Qué es PersistentIdentifier frente a mi propio UUID?

PersistentIdentifier es el ID de fila en proceso de SwiftData; se genera automáticamente y sobrevive durante la vida del proceso en ejecución. Tu propio UUID con @Attribute(.unique) es un identificador estable entre procesos y entre dispositivos. Usa PersistentIdentifier para referencias en proceso dentro de la app. Usa un UUID para cualquier cosa que cruce una frontera de proceso (sincronización entre dispositivos, integraciones externas, herramientas MCP, llamadas de red).

Referencias


  1. Get Bananas del autor, una app de lista de compras en SwiftUI que empareja SwiftData con sincronización JSON en iCloud Drive y un servidor MCP. El modelo ShoppingItem evolucionó a lo largo del ciclo de desarrollo temprano; el campo lastModified: Date? se agregó después del esquema inicial (commit 268a00d del 2025-12-01, “Make lastModified optional to fix migration crash”) porque hacerlo no opcional rompía la migración cuando las filas existentes no tenían valor con el cual poblarlo. 

  2. Apple Developer, “SwiftData” y “Adding and editing persistent data in your app”. La macro @Model, la superficie de restricciones de @Attribute y la relación con el NSManagedObjectModel de Core Data. 

  3. Apple Developer, “Preserving your app’s model data across launches” y “Adopting SwiftData for a Core Data app”. La semántica de la migración ligera y qué hace que el framework se detenga. 

  4. Apple Developer, “VersionedSchema” y “SchemaMigrationPlan”. Las declaraciones de esquema versionado, las definiciones de etapas de migración y el constructor de ModelContainer que recibe un plan de migración. 

  5. Apple Developer, “Defining data relationships with enumerations and model classes” y “Schema.Relationship”. La macro @Relationship, las opciones de deleteRule (.cascade, .nullify, .deny, .noAction) y el papel del parámetro inverse: en el mantenimiento de relaciones bidireccionales. 

  6. Análisis del autor en Dos ecosistemas de agentes, una lista de compras, 29 de abril de 2026, y Cinco plataformas de Apple, tres archivos compartidos. Los patrones de sincronización entre procesos y entre dispositivos de Get Bananas + Return que complementan (y a veces reemplazan) a SwiftData dentro de un flujo de trabajo multiproceso. 

  7. Apple Developer, “PersistentIdentifier” (conforma a Sendable) y “ModelActor”. El equipo de SwiftData confirmó durante el SwiftData Group Lab de la WWDC 2026 que los objetos @Model no son Sendable y no deberían forzarse a conformar, porque son un grafo de referencias que vive dentro de un contexto; el contrato de frontera recomendado es pasar el PersistentIdentifier Sendable más los valores planos extraídos y volver a hacer fetch en el contexto de destino, y que pasar el grafo del modelo deja al receptor con un objeto parcialmente hidratado. Parafraseado de una grabación transcrita localmente del SwiftData Group Lab de la WWDC 2026; Apple no publica subtítulos oficiales para los labs. 

  8. Apple Developer, “Adopting SwiftData for a Core Data app”, que indica que con la configuración por defecto “SwiftData copia el almacén existente al contenedor de app group”, mientras que una URL de almacén personalizada deja la ubicación para que tú la gestiones. El requisito del entitlement de CloudKit para los miembros del app group y la división en dos ModelConfiguration (uno sincronizado, uno local) para mantener a los widgets y extensions fuera de la ruta de sincronización se describieron durante el SwiftData Group Lab de la WWDC 2026. Parafraseado de una grabación transcrita localmente del SwiftData Group Lab de la WWDC 2026; Apple no publica subtítulos oficiales para los labs. 

Artículos relacionados

Migraciones de SwiftData: lightweight vs. personalizadas, y cuándo no necesitas un V2

El modelo de migración de SwiftData usa VersionedSchema, MigrationStage y SchemaMigrationPlan. La mayoría de los cambios…

14 min de lectura

SwiftData en iOS 27: Observación e historial

iOS 27 le da a SwiftData observación de cambios de primera clase con ResultsObserver, observación de historial persisten…

12 min de lectura

La capa de limpieza es el verdadero mercado de los agentes de IA

Charlie Labs pasó de construir agentes a limpiar lo que dejan. El mercado de agentes de IA se está moviendo de la genera…

15 min de lectura