O custo real do SwiftData é a disciplina de esquema
O ShoppingItem do Get Bananas é o exemplo canônico de por que a disciplina de esquema importa no SwiftData. O esquema original não incluía um carimbo de data/hora lastModified; acrescentá-lo depois exigiu um formato de migração específico, porque já havia dados em disco, e o campo foi tornado opcional justamente para corrigir uma falha de migração que apareceu quando ele foi adicionado pela primeira vez como não opcional.1
A API do SwiftData são duas macros. @Model em uma classe a torna um tipo persistente. @Attribute(.unique) em uma propriedade lhe dá uma restrição de unicidade. O framework esconde o gerenciamento da stack do Core Data, a dança dos value transformers e o boilerplate do NSManagedObjectContext. O que o framework não esconde é a migração de esquema; ele apenas torna a migração declarativa em vez de imperativa. O custo de não prestar atenção às migrações é aquele bug que apaga os dados de um usuário numa atualização de rotina.
A tese: SwiftData é barato para começar e caro para migrar de qualquer jeito. A disciplina está na nomenclatura, na opcionalidade e no VersionedSchema desde o primeiro dia, não a partir do dia em que você percebe que deveria tê-lo adotado.
Em resumo
- A macro
@Modeltransforma uma classe em um tipo persistente do SwiftData. O framework gera o esquema em tempo de compilação a partir das declarações de propriedades. - Acrescentar uma nova propriedade opcional é uma migração sem esforço: a migração leve do SwiftData dá conta. Acrescentar uma propriedade não opcional a um esquema existente exige um
VersionedSchemamais umMigrationPlanque diga ao framework como preencher o novo campo nas linhas já existentes. - Pular o
VersionedSchemadesde o primeiro dia custa isto: qualquer mudança de esquema não trivial na v2 arrisca perder o banco de dados de um usuário, porque o caminho leve é conservador e desiste quando não consegue inferir a migração. @Attribute(.unique)é a ferramenta certa para chaves naturais (umUUIDque você gerou, um ID externo que você importou).@Relationshipé a ferramenta certa para referências pai/filho. Ambas são macros que geram por baixo dos panos o encanamento correto do Core Data.2
O que @Model realmente faz
Um tipo do SwiftData é uma classe Swift com a macro @Model aplicada. O ShoppingItem do Get Bananas mostra o formato canônico:
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()
}
}
Três detalhes desse formato que a API esconde.
@Model não exige uma declaração de esquema separada para o store persistente. O SwiftData lê a definição da classe em tempo de compilação e sintetiza o esquema. As propriedades da classe viram os atributos do modelo; seus tipos Swift viram os tipos de coluna. Não há arquivo .xcdatamodeld para manter (embora o NSManagedObjectModel do Core Data continue existindo por baixo e seja ele que sustenta o esquema em tempo de execução).2
@Attribute(.unique) é uma restrição sobre uma única coluna, não uma declaração de PRIMARY KEY. A identidade persistente do SwiftData é o PersistentIdentifier, gerado automaticamente por linha. A declaração @Attribute(.unique) diz ao framework: “esta coluna guarda no máximo uma linha por valor”. Quando você insere um modelo com um valor .unique que já existe, o SwiftData faz um upsert: a linha existente é atualizada em vez de rejeitada. Essa semântica importa no código de produto: .unique não é uma validação de interface que impede o envio de duplicatas; é uma garantia de armazenamento de “no máximo um” que mescla em silêncio. O padrão id: UUID acima é o recomendado para sincronização entre processos (quando você quer um identificador estável que sobreviva ao desaparecimento do PersistentIdentifier interno ao processo), e o comportamento de upsert é exatamente o que você quer quando o mesmo UUID chega por dois caminhos de sincronização.
Classes @Model são tipos por referência, não tipos por valor. Alterar uma propriedade em uma instância de ShoppingItem dispara o rastreamento de mudanças do SwiftData; o framework registra a mudança e a persiste no próximo salvamento do contexto. A integração com SwiftUI via @Query renderiza de novo qualquer view que observe o predicado correspondente. O padrão lembra o @Observable (tratado em What SwiftUI Is Made Of), com persistência empilhada por cima.
Campos opcionais são a migração barata
O campo lastModified: Date? do ShoppingItem é opcional, e essa opcionalidade sustenta a estrutura toda. O campo foi acrescentado depois do lançamento da v1 para dar suporte à sincronização entre dispositivos e à resolução de conflitos; as linhas que já existiam nos aparelhos dos usuários não tinham valor algum de lastModified. Um campo opcional sem valor padrão deixa a migração leve do SwiftData resolver a adição sem escrever nenhuma linha de código de migração: as linhas existentes recebem nil; as novas recebem o que o init definir.3
O caminho da migração leve é o caminho educado do framework. O SwiftData inspeciona o novo esquema e o store persistente, infere a menor mudança compatível e a aplica. A migração é automática; o usuário não vê nada; o app abre normalmente sobre os dados existentes. Os casos que o caminho leve resolve com limpeza:
- Acrescentar uma propriedade opcional
- Remover uma propriedade (os dados são descartados; as leituras existentes não enxergam mais a coluna)
- Renomear um atributo que o framework consegue casar por meio de uma dica (usando
@Attribute(originalName: ...)) - Renomear uma classe
@Modelque o framework consegue casar (usando@Model.originalNameou uma dica)
Os casos em que o caminho leve desiste:
- Acrescentar a um esquema existente uma propriedade não opcional sem valor padrão (as linhas existentes não têm valor com que preenchê-la)
- Mudar o tipo de uma propriedade (por exemplo,
Int→String) - Dividir um modelo em dois, ou fundir dois em um
- Qualquer coisa que exija lógica própria para migrar
Quando o caminho leve desiste, o comportamento seguro é fazer a migração falhar. O comportamento inseguro seria descartar o banco e recomeçar do zero; o framework é conservador e se recusa a fazer isso em silêncio. O usuário vê o app quebrar na abertura com um erro de migração; quem desenvolve vê um stack trace apontando para a incompatibilidade de esquema; ninguém perde dados, mas todo mundo perde confiança.
O custo de pular o VersionedSchema desde o primeiro dia aparece na fronteira v2 → v3, quando você acrescenta a terceira funcionalidade cuja mudança de esquema ultrapassa o que o caminho leve dá conta.
VersionedSchema e MigrationPlan: a disciplina do primeiro dia
VersionedSchema declara uma versão específica do esquema do modelo. MigrationPlan declara como migrar de uma versão para a seguinte.4 O formato:
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)
]
}
As próprias classes de modelo se mudam para o namespace do esquema versionado:
extension SchemaV1 {
@Model
final class ShoppingItemV1 { /* v1 fields */ }
}
extension SchemaV2 {
@Model
final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}
O ModelContainer é construído com o plano de migração:
let container = try ModelContainer(
for: ShoppingItemV2.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration("ShoppingList")
)
O plano de migração dá ao framework um grafo tipado de como o esquema evolui. Quando o app na v2 é aberto sobre um banco na v1, o framework percorre o plano de migração, aplica os estágios nomeados e leva o banco até a v2. Quando você publicar a v3, acrescenta SchemaV3.self a schemas e um novo MigrationStage entre v2 e v3. O modelo completo de migração — quais mudanças são automáticas, quais precisam de um estágio declarado e a falha de checksum que você leva ao declarar uma V2 de que não precisava — é o tema do artigo companheiro SwiftData migrations: lightweight vs custom.
A disciplina é publicar o VersionedSchema já na v1, mesmo quando existe apenas uma versão. Isso custa um arquivo a mais e uma declaração enum a mais. Não fazer isso custa o seguinte: a primeira mudança de esquema não trivial da v2 obriga a embrulhar a v1 retroativamente em um VersionedSchema, o que é viável, mas exige cuidado para reproduzir o formato exato da v1, de modo que o framework identifique os dados existentes como SchemaV1. O seu eu do futuro, ocupado com a v2, vai pagar o imposto; o seu eu de hoje pode pagá-lo uma vez e esquecer o assunto.
MigrationStage personalizado para os casos difíceis
Migrações leves cobrem a maioria das mudanças aditivas. Mudanças de tipo, divisões, fusões e preenchimentos condicionais pedem um 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()
}
)
]
As duas closures disparam antes e depois de o framework aplicar a migração estrutural. willMigrate roda contra o esquema v1; didMigrate roda contra o esquema v2. O corpo da closure é código SwiftData comum (fetch descriptors, salvamentos do model context, as mesmas APIs usadas no app em execução), operando sobre um contexto transitório de migração.
O padrão que sobrevive em produção mantém willMigrate vazio e coloca toda a lógica de preenchimento em didMigrate. Ler dados da v1 dentro de willMigrate é permitido, mas o esquema v2 ainda não existe do ponto de vista do framework, então qualquer cálculo precisa ser depositado em um armazenamento transitório que a closure didMigrate consiga ler. A regra mais simples: migrações estruturais são trabalho do framework; preencher campos exclusivos da v2 nas linhas existentes é trabalho do didMigrate.
Quando @Attribute e @Relationship merecem seus nomes
Duas macros fazem quase todo o trabalho de decoração de esquema nas classes @Model.
@Attribute decora uma única propriedade com uma restrição ou uma dica:
@Attribute(.unique)impõe unicidade, como emShoppingItem.id@Attribute(.externalStorage)guarda blobsDatagrandes fora do banco (dados de imagem, buffers de áudio)@Attribute(originalName: "old_field_name")casa uma propriedade com uma coluna renomeada durante a migração@Attribute(.transformable(by: ...))aplica umValueTransformera um tipo que não é Codable
A disciplina certa: use .unique para campos que realmente devem ser únicos (um UUID que você gerou, um ID externo), .externalStorage para qualquer blob acima de alguns KB, e originalName quando um rename de propriedade na v2 fosse, de outro modo, perder os dados da v1.
@Relationship decora uma propriedade que aponta para outra classe @Model ou para uma coleção delas:
@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?
}
deleteRule: .cascade significa que apagar a List pai apaga todas as linhas ShoppingItem filhas. O parâmetro inverse: diz ao framework qual propriedade do filho aponta de volta para o pai; o framework usa isso para uma manutenção bidirecional previsível. O SwiftData às vezes consegue inferir a inversa sozinho, e inverse: nil é suportado para relacionamentos explicitamente unidirecionais, mas o padrão seguro é declarar inverse: sempre que a inferência ficaria ambígua.5
A disciplina certa: declare relacionamentos com deleteRule explícito (o padrão é .nullify, que raramente é o que você quer) e declare inverse: sempre que o relacionamento for bidirecional (em vez de confiar na inferência do framework). Os padrões implícitos costumam estar errados; a forma explícita é um parâmetro a mais e um bug economizado para sempre.
Cruzando uma fronteira de ator: envie o identificador, não o grafo
Uma classe @Model não é Sendable, e o certo é parar de tentar torná-la. A instância é uma referência para um grafo de objetos vivo, mantido por um ModelContext; o framework não tem como prometer que esse grafo seja seguro de ler a partir de outro ator, então o tipo fica deliberadamente sem Sendable. Forçar a conformidade não faz a corrida de dados sumir; apenas a esconde.7
O padrão que funciona é enviar a identidade e os valores simples, e então buscar de novo do outro lado. PersistentIdentifier é Sendable, então atravessa a fronteira sem sujeira. Extraia os valores escalares de que o destino precisa (um nome, uma flag, um delta em uma struct pequena), passe-os junto com o identificador e deixe o ator receptor buscar o modelo no próprio contexto usando esse 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
O modo de falha a evitar é passar o próprio grafo do modelo. Quando parte do grafo cruza a fronteira, quem recebe fica com um modelo que se hidrata só parcialmente do outro lado: relacionamentos e propriedades carregadas de forma preguiçosa que nunca foram materializados no contexto de origem se resolvem contra o contexto errado (ou nem se resolvem), e os bugs que vêm depois são do tipo silencioso. O identificador mais os valores extraídos é o contrato seguro; o grafo não é. Um ModelActor encapsula essa disciplina ao possuir um contexto e entregar valores em vez de instâncias.7
Sincronização com CloudKit e a armadilha do entitlement de App Group
Mover um store do SwiftData para um contêiner de App Group, para que um widget ou uma extensão consiga lê-lo, interage com a sincronização do CloudKit de um jeito que morde os apps depois do lançamento. Dois fatos fazem o resto se deduzir.
Primeiro, a localização do store. Com a ModelConfiguration padrão, o SwiftData copia para você o store existente para dentro do contêiner de App Group quando um app evolui de “sem grupo” para App Group; a formulação da Apple é que o SwiftData “copies the existing store to the app group container”.8 Com uma URL de store personalizada, a localização é sua: você copia o arquivo para o novo contêiner e aponta a configuração para lá. O caminho padrão é o cômodo justamente porque o framework faz a cópia; o caminho personalizado troca essa comodidade por controle.
Segundo, o entitlement. Todo membro do App Group que lê um store sincronizado com o CloudKit precisa carregar o mesmo entitlement do CloudKit, porque cada um desses processos vai sincronizar aquele contêiner por conta própria. Esse requisito é a armadilha: um widget ou uma extensão não tem orçamento de execução nem janela em primeiro plano para conduzir uma sincronização verbosa, e entregar a ele o entitlement do CloudKit o obriga a tentar mesmo assim. A saída é dividir em duas instâncias de ModelConfiguration: um store sincronizado (com o entitlement do CloudKit, pertencente ao app principal) e um store local no contêiner de App Group que o widget e as extensões leem sem nunca sincronizar. Coloque a sincronização onde um app em primeiro plano consegue fazê-la bem, e mantenha fora do caminho de sincronização os dados compartilhados apenas para leitura.8
O que eu construiria de outro jeito
Três padrões que os apps do cluster ou entregam, ou gostariam de ter entregado.
Publique VersionedSchema desde a v1. Toda classe @Model que vai para produção deveria viver dentro de um VersionedSchema desde o primeiro dia. O custo é um enum de embrulho por versão de esquema. O benefício é que a primeira mudança não trivial da v2 vira uma adição de uma linha em MigrationPlan.schemas em vez de uma refatoração retroativa de dois dias.
Torne opcional cada carimbo de data/hora. Campos como lastModified, createdAt e updatedAt, que existem para sincronização entre dispositivos ou resolução de conflitos, deveriam ser opcionais na v1 se o produto da v1 não precisa deles. A opcionalidade mantém barata a migração para a v2 (quando você de fato precisar deles). Preenchê-los nas linhas existentes durante o didMigrate é um laço; torná-los não opcionais desde a v1 é uma restrição capaz de quebrar o preenchimento retroativo sobre dados de usuários.
Use UUIDs como chave natural, não o PersistentIdentifier. O PersistentIdentifier do SwiftData vive dentro do processo. Sincronização entre dispositivos, integração com MCP (tratada em Two Agent Ecosystems, One Shopping List) e qualquer referência fora do processo precisam de um identificador estável. Um UUID com @Attribute(.unique) tem o formato certo; o PersistentIdentifier interno ao processo tem o formato errado para tudo o que cruza uma fronteira de processo.
Quando @Model é a resposta errada
Três casos em que o SwiftData não é a ferramenta certa:
Estado chave/valor de registro único. Configurações do app, o idioma escolhido pelo usuário, o carimbo de data/hora da última sincronização. Use UserDefaults ou NSUbiquitousKeyValueStore (tratados em Five Apple Platforms, Three Shared Files). O overhead do SwiftData para uma única linha é cerimônia desperdiçada; armazenamentos chave-valor são o substrato certo.
Dados com autoridade no servidor, sem escrita offline. Uma lista buscada de uma API REST e exibida somente para leitura. O SwiftData é exagero se a fonte da verdade é o servidor e o cache local é só um cache. Um simples snapshot Codable em Documents/ mais um array em memória basta; o imposto de migração do SwiftData não vale a pena se os dados não precisam sobreviver a um reset completo.
Coordenação entre processos. O SwiftData opera dentro de um processo. Um servidor MCP rodando fora do app iOS não consegue ler nem escrever no contêiner SwiftData do app. Estado entre processos precisa de outro formato: um arquivo JSON no iCloud Drive, um contêiner de App Group compartilhado, ou uma camada de sincronização explícita que faça a ponte entre processos. (O Get Bananas combina SwiftData com JSON no iCloud Drive exatamente por essa razão.)6
Os dados são blobs grandes que mudam pouco. Um arquivo de áudio de 10 MB, um conjunto de imagens de 50 MB. Use @Attribute(.externalStorage) se os blobs ficam dentro de linhas do SwiftData; caso contrário, use o sistema de arquivos diretamente, com metadados no SwiftData apontando para URLs de arquivo.
Quando o Core Data ainda vence
O SwiftData é uma camada sobre o Core Data, não um substituto de tudo o que ele faz, e três anos depois um conjunto específico de tarefas ainda pertence ao framework mais antigo. Escolher o Core Data para uma delas não é uma decisão de legado; é a decisão correta hoje.
Agregados do lado do banco. O SwiftData não tem equivalente aos fetches do Core Data baseados em NSExpression, aqueles que empurram sum, average, min e max para dentro do SQLite para que o banco os calcule sem carregar linhas9. No SwiftData você busca e reduz em memória, o que destrói o propósito em uma tabela grande. A saída de emergência documentada é a coexistência: a Apple descreve rodar “two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”, o que permite ao lado Core Data executar o agregado empurrado para o SQL contra o arquivo que pertence ao SwiftData9. A mecânica, incluindo a exigência do NSPersistentHistoryTrackingKey, está em SwiftData performance is a storage problem.
Compartilhamento e o banco público do CloudKit. A sincronização automática com o iCloud do SwiftData anda por baixo sobre o NSPersistentCloudKitContainer, e o contêiner que ele configura espelha o seu store para o banco privado do CloudKit do usuário10. A colaboração entre usuários diferentes do iCloud via CKShare, e a publicação no banco público, são capacidades documentadas de Core Data + CloudKit sem nenhuma API no nível do SwiftData até as betas do iOS 2710. Um app cuja funcionalidade central são listas compartilhadas ou documentos colaborativos ou desce para o Core Data nos stores sincronizados, ou constrói a camada CloudKit na mão.
Atualizações em lote no nível do store. O NSBatchUpdateRequest do Core Data reescreve as linhas correspondentes direto no store, sem carregar objetos11. O ModelContext do SwiftData tem a metade de exclusão (delete(model:where:) aceita um predicado), mas nenhuma contraparte de atualização em lote, então reescrever um campo em massa no SwiftData significa materializar cada modelo afetado.
Um alvo de implantação abaixo do iOS 17. O SwiftData exige iOS 17; o Core Data alcança tão longe quanto o app ainda der suporte, e seu contêiner CloudKit chega até o iOS 131012. Uma base de código com cauda longa de sistemas não tem escolha.
Vale nomear o que saiu dessa lista: “você precisa do NSFetchedResultsController fora de uma view” era um ponto do Core Data até as betas do iOS 27 acrescentarem o ResultsObserver, que observa um fetch em qualquer lugar do app por meio do Swift Observation13. A lista de lacunas encolhe a cada versão. A aposta: comece apps novos no SwiftData, adote a coexistência para as tarefas acima e trate uma stack completa de Core Data como resposta apenas quando compartilhamento, banco público ou alvo de implantação forçarem.
O que o padrão significa para apps publicados no iOS 26+
Três conclusões.
-
As macros são a parte fácil. As migrações são o custo.
@Modele@Attributesão declarações de duas linhas que escondem um bocado de encanamento do Core Data. A disciplina de migração é o que você realmente paga ao longo da vida do app; projete a v1 pensando na v2. -
VersionedSchemadesde o primeiro dia não é negociável em apps que vão para produção. Oenumde embrulho é um arquivo a mais. O custo retroativo de acrescentá-lo depois é bem maior. -
Campos opcionais e relacionamentos explícitos são o seguro barato. Carimbos de data/hora opcionais para os metadados de sincronização,
deleteRuleeinverse:explícitos nos relacionamentos. Ambos são declarações minúsculas que compram bastante flexibilidade para a v2.
O cluster completo do ecossistema Apple: App Intents tipados para a Apple Intelligence; servidores MCP para agentes entre LLMs; a questão do roteamento entre os dois; Foundation Models para LLM no dispositivo e o protocolo Tool; Live Activities para a máquina de estados da tela de bloqueio no iOS; o contrato de runtime do watchOS no Apple Watch; as entranhas do SwiftUI como substrato do framework; o modelo mental espacial do RealityKit para cenas do visionOS; os padrões do Liquid Glass para a camada visual; a publicação multiplataforma para o alcance entre dispositivos. O hub está na Apple Ecosystem Series. Para um contexto mais amplo de iOS com agentes de IA, veja o iOS Agent Development guide.
Perguntas frequentes
Qual é a diferença entre @Model e o NSManagedObject do Core Data?
@Model é uma macro Swift que gera o encanamento de NSManagedObject por baixo dos panos. O SwiftData usa o Core Data como store de apoio, então o modelo em tempo de execução é o mesmo; a diferença está na superfície. @Model elimina o arquivo .xcdatamodeld, a cerimônia dos value transformers e o gerenciamento do ciclo de vida do NSManagedObjectContext. Você fica com o mesmo store persistente e uma API com formato de Swift.
Preciso de VersionedSchema se nunca pretendo mudar o esquema?
Se o seu app talvez publique uma v2, sim. Se for uma demo de uma vez só, não. VersionedSchema desde a v1 custa uma declaração enum a mais. Acrescentá-lo retroativamente na v2 custa reproduzir o formato exato do esquema v1 para que o framework reconheça os dados existentes, o que é viável, mas propenso a erro. A maioria dos apps publicados vai acabar precisando de uma mudança de esquema; reserve orçamento para isso na v1.
Quando devo usar @Attribute(.unique)?
Quando o campo é uma chave natural da linha: um UUID que você gerou, um ID externo que você importou, um slug que você atribuiu. O SwiftData trata .unique como upsert: se você insere um modelo cujo valor .unique já existe, a linha existente é atualizada em vez de uma nova ser acrescentada. É essa semântica que torna seguros os caminhos de sincronização em estilo upsert (o mesmo UUID chegando de dois dispositivos); é também por isso que .unique é a ferramenta errada em campos de exibição como title, porque dois usuários digitando o mesmo título mesclariam suas linhas em silêncio em vez de produzir dois registros distintos.
Como lidar com um campo não opcional acrescentado a um esquema existente?
Use um MigrationStage.custom com uma closure didMigrate que preencha o campo nas linhas existentes. Ou, mais simples: declare o campo como opcional na nova versão do esquema e preencha-o de forma preguiçosa no acesso. A opcionalidade é a migração mais barata; adições não opcionais precisam de lógica de preenchimento explícita.
O que é PersistentIdentifier comparado ao meu próprio UUID?
PersistentIdentifier é o ID de linha interno ao processo do SwiftData; ele é gerado automaticamente e sobrevive enquanto durar o processo em execução. O seu próprio UUID com @Attribute(.unique) é um identificador estável entre processos e entre dispositivos. Use PersistentIdentifier para referências internas ao processo dentro do app. Use um UUID para tudo o que cruza uma fronteira de processo (sincronização entre dispositivos, integrações externas, ferramentas MCP, chamadas de rede).
Quando ainda devo escolher o Core Data em vez do SwiftData?
Quatro casos até as betas do iOS 27: agregados do lado do banco (os fetches com NSExpression que faltam ao SwiftData), compartilhamento entre usuários do iCloud via CKShare ou o banco público do CloudKit (a sincronização do SwiftData cobre o banco privado), atualizações em lote no nível do store (NSBatchUpdateRequest) e alvos de implantação abaixo do iOS 1791011. No caso dos agregados você não precisa abandonar o SwiftData: rode uma stack Core Data coexistente contra o mesmo arquivo de store.
Referências
-
Get Bananas do autor, um app SwiftUI de lista de compras que combina SwiftData com sincronização em JSON pelo iCloud Drive e um servidor MCP. O modelo
ShoppingItemevoluiu ao longo do ciclo inicial de desenvolvimento; o campolastModified: Date?foi acrescentado depois do esquema inicial (commit268a00dde 1 de dezembro de 2025, “Make lastModified optional to fix migration crash”) porque torná-lo não opcional quebrava a migração quando as linhas existentes não tinham valor com que preenchê-lo. ↩ -
Apple Developer, “SwiftData” e “Adding and editing persistent data in your app”. A macro
@Model, a superfície de restrições do@Attributee a relação com oNSManagedObjectModeldo Core Data. ↩↩ -
Apple Developer, “Preserving your app’s model data across launches” e “Adopting SwiftData for a Core Data app”. A semântica da migração leve e o que faz o framework desistir. ↩
-
Apple Developer, “VersionedSchema” e “SchemaMigrationPlan”. As declarações de esquemas versionados, as definições de estágios de migração e o construtor de
ModelContainerque recebe um plano de migração. ↩ -
Apple Developer, “Defining data relationships with enumerations and model classes” e “Schema.Relationship”. A macro
@Relationship, as opções dedeleteRule(.cascade,.nullify,.deny,.noAction) e o papel do parâmetroinverse:na manutenção de relacionamentos bidirecionais. ↩ -
Análise do autor em Two Agent Ecosystems, One Shopping List, 29 de abril de 2026, e Five Apple Platforms, Three Shared Files. Os padrões de sincronização entre processos e entre dispositivos do Get Bananas e do Return que complementam (e às vezes substituem) o SwiftData dentro de um fluxo de trabalho multiprocesso. ↩
-
Apple Developer, “PersistentIdentifier” (em conformidade com
Sendable) e “ModelActor”. A equipe do SwiftData confirmou durante o SwiftData Group Lab da WWDC 2026 que objetos@Modelnão sãoSendablee não devem ser forçados a conformar, porque são um grafo de referências que vive dentro de um contexto; o contrato de fronteira recomendado é passar oPersistentIdentifier, que éSendable, junto com os valores simples extraídos e buscar de novo no contexto de destino, e passar o grafo do modelo deixa quem recebe com um objeto parcialmente hidratado. Parafraseado de uma gravação transcrita localmente do SwiftData Group Lab da WWDC 2026; a Apple não publica legendas oficiais dos labs. ↩↩ -
Apple Developer, “Adopting SwiftData for a Core Data app”, que afirma que, com a configuração padrão, “SwiftData copies the existing store to the app group container”, enquanto uma URL de store personalizada deixa a localização por sua conta. A exigência do entitlement do CloudKit para membros de um App Group e a divisão em duas
ModelConfiguration(uma sincronizada, outra local) para manter widgets e extensões fora do caminho de sincronização foram descritas durante o SwiftData Group Lab da WWDC 2026. Parafraseado de uma gravação transcrita localmente do SwiftData Group Lab da WWDC 2026; a Apple não publica legendas oficiais dos labs. ↩↩ -
Apple, sessão 10189 da WWDC 2023, “Migrate to SwiftData”, origem do enquadramento sobre coexistência (“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”), e Apple Developer, “NSExpression”, o mecanismo por trás dos fetches de agregados empurrados para o SQL do Core Data, para os quais o SwiftData não oferece equivalente. A lacuna foi confirmada pelo painel de engenharia do SwiftData no SwiftData Group Lab da WWDC 2026 (parafraseado de uma gravação transcrita localmente). ↩↩↩
-
Apple Developer, “Syncing model data across a person’s devices”, que afirma que “SwiftData uses the
NSPersistentCloudKitContainerclass from Core Data to handle CloudKit synchronization”; “NSPersistentCloudKitContainer” (iOS 13.0+), cujo resumo descreve o espelhamento de “select persistent stores to a CloudKit private database”; e “Sharing Core Data objects between iCloud users”, o caminho documentado do Core Data para colaboração baseada emCKShare. A documentação do SwiftData não expõe nenhuma API de compartilhamento ou de banco público até as betas do iOS 27. ↩↩↩↩ -
Apple Developer, “NSBatchUpdateRequest” e “ModelContext.delete(model:where:includeSubclasses:)”, a exclusão em lote baseada em predicado do SwiftData. A documentação do
ModelContextdo SwiftData não lista nenhuma contraparte de atualização em lote. ↩↩ -
Disponibilidade por plataforma segundo a documentação da Apple Developer: SwiftData (iOS 17.0+) e Core Data (iOS 3.0+). ↩
-
Apple Developer, “ResultsObserver” (beta do iOS 27.0), que “observes and tracks changes to a collection of persistent models in a model context” e está em conformidade com
Observable, cumprindo o papel de observação fora de uma view que antes exigia oNSFetchedResultsControllerdo Core Data. ↩