O verdadeiro custo do SwiftData é a disciplina de schema
O ShoppingItem do Get Bananas é o exemplo canônico de por que a disciplina de schema do SwiftData importa. O schema original não incluía um timestamp lastModified; adicioná-lo depois exigiu um formato específico de migração porque os dados existentes já estavam em disco, e o campo foi tornado opcional especificamente para corrigir um crash de migração que ocorreu 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 transforma em 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 schema; ele apenas torna a migração declarativa em vez de imperativa. O custo de não prestar atenção às migrações é o bug que apaga os dados de um usuário em uma atualização de rotina.
A tese: o SwiftData é barato para começar e caro para migrar de forma desleixada. A disciplina está na nomenclatura, na opcionalidade e no VersionedSchema desde o primeiro dia, não no dia em que você percebe que deveria ter feito isso.
TL;DR
- A macro
@Modeltransforma uma classe em um tipo persistente do SwiftData. O framework gera o schema a partir das declarações de propriedade em tempo de compilação. - Adicionar uma nova propriedade opcional é uma migração nula: a lightweight migration do SwiftData cuida disso. Adicionar uma propriedade não opcional a um schema existente requer um
VersionedSchemamais umMigrationPlanque diga ao framework como popular o novo campo para as linhas existentes. - O custo de pular o
VersionedSchemadesde o primeiro dia é que qualquer mudança de schema v2 não trivial corre o risco de derrubar o banco de dados de um usuário, porque o caminho lightweight é 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 o encanamento correto do Core Data nos bastidores.2
O que @Model realmente faz
Um tipo do SwiftData é uma classe Swift com a macro @Model aplicada. O ShoppingItem do Get Bananas é 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 sobre esse formato que a API esconde.
@Model não requer uma declaração separada de schema do persistent store. O SwiftData lê a definição da classe em tempo de compilação e sintetiza o schema. As propriedades da classe se tornam os atributos do modelo; seus tipos Swift se tornam os tipos de coluna. Não há nenhum arquivo .xcdatamodeld para manter (embora o NSManagedObjectModel subjacente do Core Data ainda exista e seja o que sustenta o schema 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 armazena no máximo uma linha por valor.” Quando você insere um modelo com um valor .unique que já existe, o SwiftData executa um upsert: a linha existente é atualizada em vez de rejeitada. A semântica importa para o código de produto: .unique não é uma validação de nível de UI que impede que duplicatas sejam enviadas; é uma garantia de armazenamento de no-máximo-um que faz a mesclagem silenciosamente. O padrão id: UUID acima é o recomendado para sincronização entre processos (onde você quer um identificador estável que sobreviva ao desaparecimento do PersistentIdentifier em processo), e o comportamento de upsert é exatamente o que você quer quando o mesmo UUID chega por dois caminhos de sync.
Classes @Model são reference types, não value types. Mutar uma propriedade em uma instância de ShoppingItem aciona o change tracking do SwiftData; o framework registra a mudança e a persiste no próximo save do context. A integração com SwiftUI por meio de @Query re-renderiza qualquer view que observe o predicate correspondente. O padrão é similar ao @Observable (abordado em Do que o SwiftUI é feito), com persistência sobreposta no topo.
Campos opcionais são a migração barata
O campo lastModified: Date? em ShoppingItem é opcional, e a opcionalidade é estrutural. O campo foi adicionado depois que a v1 foi publicada para dar suporte a sync entre dispositivos e resolução de conflitos; as linhas existentes nos dispositivos dos usuários não tinham nenhum valor de lastModified. Um campo opcional sem valor padrão deixa a lightweight migration do SwiftData lidar com a adição sem escrever nenhum código de migração: as linhas existentes recebem nil; as novas linhas recebem o que o init definir.3
O caminho da lightweight migration é o caminho gentil do framework. O SwiftData inspeciona o novo schema e o persistent store, infere a menor mudança compatível e a aplica. A migração é automática; o usuário não vê nada; o app inicia normalmente sobre os dados existentes. Os casos que o caminho lightweight lida de forma limpa:
- Adicionar uma propriedade opcional
- Remover uma propriedade (os dados são descartados; as leituras existentes não veem mais a coluna)
- Renomear um atributo que o framework consegue casar por dica (usando
@Attribute(originalName: ...)) - Renomear uma classe
@Modelque o framework consegue casar (usando@Model.originalNameou uma dica)
Os casos em que o caminho lightweight desiste:
- Adicionar uma propriedade não opcional sem valor padrão a um schema existente (as linhas existentes não têm valor para populá-la)
- Mudar o tipo de uma propriedade (por exemplo,
Int→String) - Dividir um modelo em dois modelos, ou mesclar dois em um
- Qualquer coisa que exija lógica customizada para migrar
Quando o caminho lightweight desiste, o comportamento seguro é falhar a migração. O comportamento inseguro seria derrubar o banco de dados e começar do zero; o framework é conservador e se recusa a fazer isso silenciosamente. O usuário vê o app crashar na inicialização com um erro de migração; o desenvolvedor vê um stack trace apontando para a incompatibilidade de schema; ninguém perde dados, mas todo mundo perde a confiança.
O custo de pular o VersionedSchema desde o primeiro dia aparece na fronteira v2 → v3, quando você adiciona a terceira funcionalidade cuja mudança de schema excede o que o caminho lightweight lida.
VersionedSchema e MigrationPlan: a disciplina do primeiro dia
VersionedSchema declara uma versão específica do schema do modelo. MigrationPlan declara como migrar de uma versão para a próxima.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 migram para o namespace do versioned-schema:
extension SchemaV1 {
@Model
final class ShoppingItemV1 { /* v1 fields */ }
}
extension SchemaV2 {
@Model
final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}
O ModelContainer é construído com o migration plan:
let container = try ModelContainer(
for: ShoppingItemV2.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration("ShoppingList")
)
O migration plan dá ao framework um grafo tipado de como o schema evolui. Quando o app que publica a v2 inicia contra um banco de dados v1, o framework percorre o migration plan, aplica os stages nomeados e leva o banco de dados até a v2. Quando você publica a v3, você adiciona SchemaV3.self a schemas e um novo MigrationStage entre a v2 e a v3.
A disciplina é publicar o VersionedSchema na v1, mesmo quando existe apenas uma versão. O custo de fazer isso é um arquivo extra e uma declaração de enum extra. O custo de não fazer isso é que a primeira mudança de schema não trivial da v2 exige envolver retroativamente a v1 em um VersionedSchema, o que é viável mas requer cuidado para casar o formato exato da v1 de modo que o framework consiga identificar os dados existentes como SchemaV1. O você-do-futuro trabalhando na v2 vai pagar o imposto; o você-do-presente pode pagá-lo uma vez e esquecer.
MigrationStage customizado para os casos difíceis
As lightweight migrations cobrem a maioria das mudanças aditivas. Mudanças de tipo, divisões, mesclagens e populações condicionais precisam de 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 schema v1; didMigrate roda contra o schema v2. O corpo da closure é código SwiftData normal (fetch descriptors, saves de model context, as mesmas APIs usadas no app em execução), operando contra um context transitório em-migração.
O padrão que sobrevive em produção é manter willMigrate vazio e colocar toda a lógica de população em didMigrate. Ler dados v1 dentro de willMigrate é permitido, mas o schema v2 ainda não existe da perspectiva do framework, então qualquer computação precisa ser preparada em um store transitório que a closure didMigrate consiga ler. A regra mais simples: migrações estruturais são tarefa do framework; popular campos exclusivos da v2 em linhas existentes é tarefa de didMigrate.
Quando @Attribute e @Relationship merecem seus nomes
Duas macros fazem a maior parte do trabalho de decoração de schema em classes @Model.
@Attribute decora uma única propriedade com uma restrição ou dica:
@Attribute(.unique)impõe unicidade, como emShoppingItem.id@Attribute(.externalStorage)armazena grandes blobs deDatafora do banco de dados (dados de imagem, buffers de áudio)@Attribute(originalName: "old_field_name")casa uma propriedade a uma coluna renomeada durante a migração@Attribute(.transformable(by: ...))aplica umValueTransformera um tipo não Codable
A disciplina certa: use .unique para campos que genuinamente deveriam ser únicos (um UUID que você gerou, um ID externo), use .externalStorage para qualquer blob acima de alguns KB, use originalName quando a renomeação de uma propriedade na v2 de outra forma perderia 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?
}
O deleteRule: .cascade significa que deletar o List pai deleta todas as linhas filhas de ShoppingItem. O parâmetro inverse: diz ao framework qual propriedade no filho aponta de volta para o pai; o framework o usa para uma manutenção bidirecional previsível. O SwiftData às vezes consegue inferir o inverse automaticamente, e inverse: nil é suportado para relacionamentos explicitamente unidirecionais, mas o padrão seguro é declarar inverse: sempre que a inferência for 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 geralmente estão errados; a forma explícita é um parâmetro extra e um bug salvo para sempre.
Cruzando uma fronteira de actor: envie o identificador, não o grafo
Uma classe @Model não é Sendable, e a jogada certa é 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 pode prometer que esse grafo seja seguro para ler a partir de outro actor, então o tipo é deliberadamente deixado como não Sendable. Forçar a conformidade não faz a data race desaparecer; ela a esconde.7
O padrão que funciona é enviar a identidade e os valores simples, e então refazer o fetch do outro lado. PersistentIdentifier é Sendable, então ele cruza a fronteira de forma limpa. Extraia quaisquer valores escalares de que o destino precise (um nome, uma flag, um delta em uma pequena struct), passe-os junto com o identificador, e faça o actor receptor refazer o fetch do modelo a partir de seu próprio context usando o 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, o receptor recebe um modelo que se hidrata parcialmente do outro lado: relacionamentos e propriedades carregadas preguiçosamente que nunca foram resolvidas no context de origem resolvem contra o context errado (ou de jeito nenhum), e os bugs que se seguem 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 context e fornecer valores em vez de instâncias.7
Sync do CloudKit e a armadilha do entitlement de app group
Mover um store do SwiftData para um container de app group para que um widget ou extension possa lê-lo interage com o sync do CloudKit de uma maneira que morde os apps depois que eles são publicados. Dois fatos fazem o resto seguir.
Primeiro, a localização do store. Com o ModelConfiguration padrão, o SwiftData copia o store existente para o container do app group por você quando um app evolui de sem-grupo para app group; a formulação da Apple é que o SwiftData “copia o store existente para o container do app group.”8 Com uma URL de store customizada, você é dono da localização: você copia o arquivo para o novo container e aponta a configuração para ele você mesmo. O caminho padrão é o conveniente precisamente porque o framework faz a cópia; o caminho customizado troca essa conveniência 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 container por conta própria. Esse requisito é a armadilha: um widget ou extension não tem o orçamento de runtime nem a janela em foreground para conduzir um sync verboso, e entregar a ele o entitlement do CloudKit o força a tentar. A correção é dividir em duas instâncias de ModelConfiguration: um store sincronizado (entitlement do CloudKit, de propriedade do app principal) e um store local no container do app group que o widget e as extensions leem sem nunca sincronizar. Coloque o sync onde um app em foreground consegue fazê-lo bem, e mantenha os dados compartilhados-para-leitura fora do caminho de sync.8
O que eu construiria de forma diferente
Três padrões que os apps do cluster ou publicam ou desejariam ter publicado.
Publique VersionedSchema desde a v1. Toda classe @Model que vai a produção deveria viver dentro de um VersionedSchema desde o primeiro dia. O custo é um enum envolvente por versão de schema. O benefício é que a primeira mudança não trivial da v2 é uma adição de uma linha a MigrationPlan.schemas em vez de uma refatoração retroativa de dois dias.
Torne todo timestamp opcional. Campos como lastModified, createdAt e updatedAt que existem para sync entre dispositivos ou resolução de conflitos deveriam ser opcionais na v1 se o produto v1 não precisa deles. A opcionalidade mantém barata a migração para a v2 (quando você de fato precisa deles). Preenchê-los em linhas existentes durante didMigrate é um loop; torná-los não opcionais desde a v1 é uma restrição que pode quebrar o backfill nos dados do usuário.
Use UUIDs como chave natural, não o PersistentIdentifier. O PersistentIdentifier do SwiftData é em processo. Sync entre dispositivos, integração com MCP (abordada em Dois ecossistemas de agentes, uma lista de compras), e qualquer referência fora-de-processo precisam de um identificador estável. Um UUID com @Attribute(.unique) é o formato certo; o PersistentIdentifier em processo é o formato errado para qualquer coisa que cruze 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 selecionado pelo usuário, o timestamp do último sync. Use UserDefaults ou NSUbiquitousKeyValueStore (abordados em Cinco plataformas Apple, três arquivos compartilhados). O overhead do SwiftData para uma única linha é cerimônia desperdiçada; os stores chave-valor são o substrato certo.
Dados autoritativos no servidor sem escritas offline. Uma lista buscada de uma REST API e exibida somente leitura. O SwiftData é exagero se a fonte da verdade é o servidor e o cache local é apenas um cache. Um simples snapshot Codable em Documents/ mais um array em cache na memória é suficiente; o imposto de migração do SwiftData não vale a pena pagar se os dados não sobrevivem a um reset completo.
Coordenação multiprocesso. O SwiftData opera dentro de um processo. Um servidor MCP rodando fora do app iOS não consegue ler nem escrever no container do SwiftData do app. Estado entre processos precisa de um formato diferente: um arquivo JSON no iCloud Drive, um container de App Group compartilhado, ou uma camada de sincronização explícita que faça a ponte entre processos. (O Get Bananas combina o SwiftData com JSON no iCloud Drive exatamente por esse motivo.)6
Os dados são grandes blobs que mudam raramente. Um arquivo de áudio de 10MB, um dataset de imagem de 50MB. Use @Attribute(.externalStorage) se os blobs estiverem dentro de linhas do SwiftData; caso contrário, use o filesystem diretamente com metadados no SwiftData apontando para URLs de arquivos.
O que o padrão significa para apps publicando 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 muito encanamento do Core Data. A disciplina de migração é o que você de fato paga ao longo da vida do app; projete a v1 com a v2 em mente. -
VersionedSchemadesde o primeiro dia é inegociável para apps em produção. Oenumenvolvente é um arquivo extra. O custo retroativo de adicioná-lo depois é muito maior. -
Campos opcionais e relacionamentos explícitos são o seguro barato. Timestamps opcionais para metadados de sync,
deleteRuleeinverse:explícitos em relacionamentos. Ambos são declarações minúsculas que compram muita flexibilidade na v2.
O cluster completo do Apple Ecosystem: App Intents tipados para o Apple Intelligence; servidores MCP para agentes cross-LLM; a questão de roteamento entre eles; Foundation Models para LLM on-device e o protocolo Tool; Live Activities para a máquina de estados da Lock Screen no iOS; o contrato de runtime do watchOS no Apple Watch; internos do SwiftUI para o substrato do framework; o modelo mental espacial do RealityKit para cenas do visionOS; padrões de Liquid Glass para a camada visual; publicação multiplataforma para alcance entre dispositivos. O hub está na Apple Ecosystem Series. Para um contexto mais amplo de iOS-com-agentes-de-IA, veja o guia de iOS Agent Development.
FAQ
Qual é a diferença entre @Model e o NSManagedObject do Core Data?
@Model é uma macro Swift que gera o encanamento do NSManagedObject nos bastidores. O SwiftData usa o Core Data como seu backing store, então o modelo de runtime é o mesmo; a diferença é a superfície. @Model remove o arquivo .xcdatamodeld, a cerimônia dos value-transformers e o gerenciamento de ciclo de vida do NSManagedObjectContext. Você obtém o mesmo persistent store com uma API com formato Swift.
Eu preciso de VersionedSchema se nunca planejo mudar o schema?
Se o seu app pode publicar uma v2, sim. Se for uma demo de uso único, não. O custo do VersionedSchema desde a v1 é uma declaração de enum extra. O custo de adicioná-lo retroativamente na v2 é casar o formato exato do schema v1 para que o framework reconheça os dados existentes, o que é viável mas propenso a erros. A maioria dos apps em produção acabará precisando de uma mudança de schema; reserve orçamento para isso na v1.
Quando eu deveria usar @Attribute(.unique)?
Quando o campo é uma chave natural para a 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 linha ser anexada. Essa semântica é o que torna seguros os caminhos de sync no estilo upsert (o mesmo UUID vindo de dois dispositivos); é também por isso que .unique é a ferramenta errada em campos de nome de exibição como title, porque dois usuários digitando o mesmo título mesclariam silenciosamente suas linhas em vez de produzir dois registros distintos.
Como eu lido com um campo não opcional adicionado a um schema existente?
Use um MigrationStage.custom com uma closure didMigrate que popula o campo nas linhas existentes. Ou, mais fácil: declare o campo como opcional na nova versão do schema e o preencha preguiçosamente no acesso. A opcionalidade é a migração mais barata; adições não opcionais precisam de lógica de população explícita.
O que é PersistentIdentifier versus o meu próprio UUID?
PersistentIdentifier é o ID de linha em processo do SwiftData; ele é gerado automaticamente e sobrevive ao tempo de vida do 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 em processo dentro do app. Use um UUID para qualquer coisa que cruze uma fronteira de processo (sync entre dispositivos, integrações externas, ferramentas MCP, chamadas de rede).
References
-
O Get Bananas do autor, um app de lista de compras em SwiftUI que combina o SwiftData com sync de JSON no iCloud Drive e um servidor MCP. O modelo
ShoppingItemevoluiu ao longo do ciclo inicial de desenvolvimento; o campolastModified: Date?foi adicionado depois do schema inicial (commit268a00dem 2025-12-01, “Make lastModified optional to fix migration crash”) porque torná-lo não opcional quebrava a migração quando linhas existentes não tinham valor para populá-lo. ↩ -
Apple Developer, “SwiftData” e “Adding and editing persistent data in your app”. A macro
@Model, a superfície de restrições de@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”. Semântica da lightweight migration e o que faz o framework desistir. ↩
-
Apple Developer, “VersionedSchema” e “SchemaMigrationPlan”. Declarações de versioned schema, definições de migration stage e o construtor de
ModelContainerque recebe um migration plan. ↩ -
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 Dois ecossistemas de agentes, uma lista de compras, 29 de abril de 2026, e Cinco plataformas Apple, três arquivos compartilhados. Os padrões de sync entre processos e entre dispositivos do Get Bananas + Return que complementam (e às vezes substituem) o SwiftData dentro de um fluxo de trabalho multiprocesso. ↩
-
Apple Developer, “PersistentIdentifier” (conforma a
Sendable) e “ModelActor”. A equipe do SwiftData confirmou durante o WWDC 2026 SwiftData Group Lab que objetos@Modelnão sãoSendablee não devem ser forçados a conformar, porque são um grafo de referência vivendo dentro de um context; o contrato de fronteira recomendado é passar oPersistentIdentifierSendablemais os valores simples extraídos e refazer o fetch no context de destino, e que passar o grafo do modelo deixa o receptor com um objeto parcialmente hidratado. Parafraseado de uma gravação transcrita localmente do WWDC 2026 SwiftData Group Lab; a Apple não publica legendas oficiais para os 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 customizada deixa a localização para você gerenciar. O requisito de entitlement do CloudKit para membros do app group e a divisão em duas instâncias de
ModelConfiguration(uma sincronizada, uma local) para manter widgets e extensions fora do caminho de sync foram descritos durante o WWDC 2026 SwiftData Group Lab. Parafraseado de uma gravação transcrita localmente do WWDC 2026 SwiftData Group Lab; a Apple não publica legendas oficiais para os labs. ↩↩