Se você trabalha com SwiftData desde o iOS 17, provavelmente já esbarrou numa limitação bem chata: a impossibilidade de usar herança de classes nos seus modelos. Sabe aquela ideia de criar uma classe base Veículo com subclasses como Carro e Moto? Pois é, o framework simplesmente não deixava. Mas essa era acabou. Com o iOS 26, a Apple finalmente trouxe suporte completo a herança de modelos no SwiftData — e, sinceramente, o resultado ficou mais elegante do que eu esperava.
Neste guia, vamos cobrir tudo: desde a definição de classes base e subclasses até queries tipadas, migração de schema versionado e aquelas considerações de performance que quase ninguém menciona. Bora lá.
Por Que Herança de Modelos era Tão Necessária?
Antes do iOS 26, o SwiftData limitava seus modelos a classes independentes. Se dois modelos compartilhavam propriedades, você tinha basicamente duas opções — e nenhuma era boa:
- Duplicar código — copiar as mesmas propriedades em cada modelo, violando o princípio DRY de um jeito que dá até calafrio
- Usar protocolos — funciona pra definir uma interface comum, mas protocolos não persistem dados por si só no SwiftData
Pra entender melhor, considere um app de gerenciamento de eventos. Todos os eventos têm title, location e scheduledDate. Mas eventos corporativos precisam de budget e departmentCode, enquanto eventos sociais precisam de category e guestCount. Sem herança, você acabava com modelos inflados cheios de propriedades opcionais que não faziam sentido pra metade dos registros. Ou então tinha modelos separados que duplicavam tudo.
Frustrante, né?
Com herança no SwiftData, a solução fica natural: uma classe base Event com propriedades compartilhadas, e subclasses especializadas que adicionam só o que é relevante pro seu contexto. É aquela relação clássica "é-um" (is-a) da orientação a objetos, agora totalmente integrada ao framework de persistência da Apple.
Definindo Classes Base e Subclasses no SwiftData
A sintaxe é surpreendentemente simples. A classe base usa a macro @Model normalmente, e as subclasses herdam dela como qualquer classe Swift. O detalhe importante: elas precisam da anotação @available(iOS 26, *).
Classe Base: O Modelo Pai
import SwiftData
@Model
class Event {
var title: String
var location: String
var scheduledDate: Date
var duration: TimeInterval
init(title: String, location: String, scheduledDate: Date, duration: TimeInterval) {
self.title = title
self.location = location
self.scheduledDate = scheduledDate
self.duration = duration
}
}
Nada muda na classe base. Ela continua sendo um modelo SwiftData comum, com a macro @Model gerando toda a infraestrutura de persistência automaticamente. Simples assim.
Subclasses: Modelos Especializados
@available(iOS 26, *)
@Model
class WorkEvent: Event {
var budget: Decimal = 0.0
var departmentCode: String = ""
init(title: String, location: String, scheduledDate: Date,
duration: TimeInterval, budget: Decimal, departmentCode: String) {
self.budget = budget
self.departmentCode = departmentCode
super.init(title: title, location: location,
scheduledDate: scheduledDate, duration: duration)
}
}
@available(iOS 26, *)
@Model
class SocialEvent: Event {
var category: EventCategory = .party
var guestCount: Int = 0
init(title: String, location: String, scheduledDate: Date,
duration: TimeInterval, category: EventCategory, guestCount: Int) {
self.category = category
self.guestCount = guestCount
super.init(title: title, location: location,
scheduledDate: scheduledDate, duration: duration)
}
}
enum EventCategory: String, Codable {
case party
case wedding
case birthday
case reunion
}
Repare em três pontos que fazem diferença:
@available(iOS 26, *)é obrigatório nas subclasses — herança no SwiftData só funciona a partir do iOS 26, sem exceção- Valores padrão são essenciais nas propriedades das subclasses (isso facilita a migração de registros existentes, como veremos mais adiante)
super.init()deve ser chamado pra inicializar as propriedades da classe pai
Registrando no ModelContainer
Aqui tem um detalhe que pega muita gente. Todas as classes da hierarquia precisam ser registradas no container — não basta registrar só a classe base:
// No seu @main App
@main
struct EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Event.self, WorkEvent.self, SocialEvent.self])
}
}
Se você esquecer de incluir uma subclasse no container, o SwiftData simplesmente não vai reconhecê-la na persistência. E a mensagem de erro, honestamente, nem sempre ajuda a entender o que tá errado. Então fica o aviso.
Queries com Herança: Buscas Polimórficas e Tipadas
Uma das maiores vantagens da herança no SwiftData — e talvez a que mais me animou — é a flexibilidade nas queries. Você pode buscar pela classe base (retornando todos os tipos) ou filtrar por subclasses específicas.
Busca Polimórfica: Todos os Eventos
// Retorna TODOS os eventos — Event, WorkEvent e SocialEvent
@Query(sort: \Event.scheduledDate)
private var allEvents: [Event]
Quando você faz uma query pela classe base, o SwiftData automaticamente retorna instâncias de todas as subclasses. E o melhor: cada objeto mantém seu tipo real. Ou seja, um WorkEvent retornado numa query de Event continua sendo um WorkEvent, e você pode fazer type casting normalmente.
Busca Tipada: Apenas Uma Subclasse
// Retorna APENAS os eventos sociais
@Query(sort: \SocialEvent.scheduledDate)
private var socialEvents: [SocialEvent]
Direto ao ponto.
Filtragem por Tipo com #Predicate
Uma novidade bem poderosa é o uso do operador is dentro de #Predicate pra filtrar por tipo:
// Filtra apenas WorkEvents numa query de Event
let workFilter = #Predicate { $0 is WorkEvent }
var descriptor = FetchDescriptor(predicate: workFilter)
descriptor.sortBy = [SortDescriptor(\.scheduledDate)]
let workEvents = try context.fetch(descriptor)
Isso é extremamente útil quando você tem uma lista mista mas quer filtrar dinamicamente por tipo — pensa numa interface com segmented control onde o usuário escolhe "Todos", "Trabalho" ou "Social". Cai como uma luva.
Combinando Filtros de Tipo com Predicados
// Eventos sociais com mais de 50 convidados
let bigPartyFilter = #Predicate {
$0.guestCount > 50
}
let descriptor = FetchDescriptor(
predicate: bigPartyFilter,
sortBy: [SortDescriptor(\.guestCount, order: .reverse)]
)
let bigParties = try context.fetch(descriptor)
Exemplo Completo em SwiftUI
struct EventListView: View {
@Query(sort: \Event.scheduledDate)
private var events: [Event]
var body: some View {
List(events) { event in
VStack(alignment: .leading) {
Text(event.title)
.font(.headline)
Text(event.location)
.font(.subheadline)
.foregroundStyle(.secondary)
// Type casting para exibir detalhes específicos
if let workEvent = event as? WorkEvent {
Label("Dept: \(workEvent.departmentCode)",
systemImage: "building.2")
.font(.caption)
.foregroundStyle(.blue)
} else if let socialEvent = event as? SocialEvent {
Label("\(socialEvent.guestCount) convidados",
systemImage: "person.3")
.font(.caption)
.foregroundStyle(.green)
}
}
}
}
}
Repare como o type casting com as? permite acessar propriedades específicas de cada subclasse de forma segura. O SwiftUI renderiza a lista certinho, exibindo informações contextuais pro tipo de cada evento.
Migração de Schema: Preservando Dados do Usuário
Se o seu app já tá em produção com um modelo Event simples (sem herança), você vai precisar migrar o schema quando introduzir subclasses. A boa notícia: o SwiftData tem um sistema de migração robusto baseado em schemas versionados.
Passo 1: Definir o Schema Atual (V1)
enum EventSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version {
Schema.Version(1, 0, 0)
}
static var models: [any PersistentModel.Type] {
[Event.self]
}
// Cópia do modelo original (sem herança)
@Model
class Event {
var title: String
var location: String
var scheduledDate: Date
var duration: TimeInterval
init(title: String, location: String,
scheduledDate: Date, duration: TimeInterval) {
self.title = title
self.location = location
self.scheduledDate = scheduledDate
self.duration = duration
}
}
}
Passo 2: Definir o Novo Schema (V2) com Herança
@available(iOS 26, *)
enum EventSchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version {
Schema.Version(2, 0, 0)
}
static var models: [any PersistentModel.Type] {
[Event.self, WorkEvent.self, SocialEvent.self]
}
}
Passo 3: Criar o Plano de Migração
enum EventMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
var all: [any VersionedSchema.Type] = [EventSchemaV1.self]
if #available(iOS 26, *) {
all.append(EventSchemaV2.self)
}
return all
}
static var stages: [MigrationStage] {
var all: [MigrationStage] = []
if #available(iOS 26, *) {
all.append(migrateV1toV2)
}
return all
}
@available(iOS 26, *)
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: EventSchemaV1.self,
toVersion: EventSchemaV2.self
)
}
Neste caso, a migração é lightweight (leve) porque estamos apenas adicionando novas subclasses. O SwiftData consegue fazer isso sozinho — não precisa transformar dados existentes. Os registros de Event que já existiam continuam sendo Event sem nenhum problema.
Passo 4: Configurar o Container com a Migração
@main
struct EventApp: App {
let container: ModelContainer
init() {
do {
let config = ModelConfiguration()
container = try ModelContainer(
for: Event.self, WorkEvent.self, SocialEvent.self,
migrationPlan: EventMigrationPlan.self,
configurations: config
)
} catch {
fatalError("Falha ao criar ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Quando Usar Migração Custom
A migração lightweight funciona pra adicionar subclasses e novas propriedades com valores padrão. Mas se você precisa converter registros existentes em subclasses — tipo transformar todos os Event que têm departmentCode preenchido em WorkEvent — aí vai precisar de uma migração custom:
@available(iOS 26, *)
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: EventSchemaV1.self,
toVersion: EventSchemaV2.self,
willMigrate: { context in
// Lógica executada ANTES da migração do schema
// Ideal para preparar dados
},
didMigrate: { context in
// Lógica executada DEPOIS da migração do schema
// Ideal para converter tipos e limpar dados
try context.save()
}
)
Confesso que na maioria dos projetos que trabalhei, a migração lightweight deu conta. Mas é bom saber que a opção custom existe pra cenários mais complexos.
Performance: O que Acontece Por Baixo dos Panos
Entender como o SwiftData implementa herança no SQLite é essencial pra tomar boas decisões. E aqui tem um trade-off importante que poucos tutoriais mencionam.
Single Table Inheritance (STI)
O SwiftData (assim como o Core Data) usa a estratégia de Single Table Inheritance. O que isso quer dizer na prática? A classe pai e todas as suas subclasses são armazenadas na mesma tabela SQLite.
No nosso exemplo, o SwiftData cria uma única tabela Event com estas colunas:
title,location,scheduledDate,duration— campos da classe basebudget,departmentCode— campos deWorkEventcategory,guestCount— campos deSocialEventZ_ENT— coluna interna que identifica o tipo real de cada registro
Quando você salva um SocialEvent, as colunas budget e departmentCode ficam NULL. E vice-versa.
Vantagens do STI
- Queries polimórficas rápidas — buscar todos os eventos é uma query simples na mesma tabela, sem JOINs
- Simplicidade — uma única tabela é bem mais fácil de gerenciar e indexar
- Transações atômicas — inserções e atualizações de qualquer tipo acontecem na mesma tabela, sem complicação
Desvantagens e Riscos
- Tabela "larga" (Wide Table) — muitas subclasses com atributos diferentes resultam numa tabela cheia de colunas NULL, desperdiçando espaço
- Índices inchados — índices compartilhados podem crescer sem necessidade, impactando inserções e atualizações
- Mudança em qualquer subclasse — adicionar uma propriedade em qualquer subclasse afeta a tabela central inteira
Na minha experiência, pra maioria dos apps isso não é um problema real. Mas se você tá modelando algo com 10+ subclasses diferentes, vale a pena repensar a abordagem.
Dicas de Otimização
Pra manter o desempenho saudável, considere estas práticas:
// 1. Busque apenas as propriedades que vai usar
var descriptor = FetchDescriptor()
descriptor.propertiesToFetch = [\.title, \.scheduledDate]
// 2. Limite a quantidade de resultados
descriptor.fetchLimit = 50
// 3. Prefetch de relacionamentos quando necessário
descriptor.relationshipKeyPathsForPrefetching = [\.venue]
// 4. Use índices para propriedades consultadas frequentemente
@Model
class Event {
// ...
static var indexes: [[IndexColumn]] {
[[IndexColumn(\Event.scheduledDate)]]
}
}
Quando Usar (e Quando NÃO Usar) Herança
Herança no SwiftData é uma ferramenta poderosa, mas não é bala de prata. Aqui vai um guia rápido pra tomar a decisão certa.
Use herança quando:
- Os modelos formam uma hierarquia natural "é-um" (um
WorkEventé umEvent) - As subclasses compartilham muitas propriedades da classe base
- Você precisa fazer queries polimórficas com frequência
- O volume de dados é moderado (dezenas de milhares de registros, no máximo)
NÃO use herança quando:
- Os modelos compartilham apenas uma ou duas propriedades — nesse caso, protocolos resolvem
- As subclasses têm atributos radicalmente diferentes entre si (cria uma tabela SQLite muito esparsa)
- Uma subclasse vai ter ordens de magnitude mais dados que as outras
- Você nunca precisa buscar tipos diferentes na mesma query
- O target mínimo do seu app é anterior ao iOS 26
Observando Mudanças em Modelos com Herança
Modelos SwiftData são automaticamente Observable, então o SwiftUI já reage a mudanças em propriedades de qualquer nível da hierarquia. Funciona "de graça". Mas se você precisa observar mudanças de forma mais granular — especialmente vindas de sincronização via CloudKit — o HistoryDescriptor é seu amigo:
@available(iOS 26, *)
func observeChanges(in context: ModelContext) throws {
let descriptor = HistoryDescriptor()
let transactions = try context.fetchHistory(descriptor)
for transaction in transactions {
for change in transaction.changes {
// Verifica mudanças em qualquer tipo da hierarquia
let entityName = change.changedPersistentIdentifier
.entityName
switch entityName {
case "Event", "WorkEvent", "SocialEvent":
print("Mudança detectada em: \(entityName)")
default:
break
}
}
}
}
Exemplo Prático Completo: App de Eventos
Pra fechar com chave de ouro, vamos juntar tudo num exemplo funcional que demonstra herança, queries tipadas e criação de diferentes tipos de eventos:
import SwiftUI
import SwiftData
struct CreateEventView: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
@State private var title = ""
@State private var location = ""
@State private var date = Date()
@State private var eventType = 0 // 0: Geral, 1: Trabalho, 2: Social
// Propriedades de WorkEvent
@State private var budget: Decimal = 0
@State private var departmentCode = ""
// Propriedades de SocialEvent
@State private var guestCount = 0
var body: some View {
NavigationStack {
Form {
Section("Informações Gerais") {
TextField("Título", text: $title)
TextField("Local", text: $location)
DatePicker("Data", selection: $date)
Picker("Tipo", selection: $eventType) {
Text("Geral").tag(0)
Text("Trabalho").tag(1)
Text("Social").tag(2)
}
.pickerStyle(.segmented)
}
if eventType == 1 {
Section("Detalhes Corporativos") {
TextField("Código do Departamento",
text: $departmentCode)
TextField("Orçamento",
value: $budget, format: .currency(code: "BRL"))
}
}
if eventType == 2 {
Section("Detalhes Sociais") {
Stepper("Convidados: \(guestCount)",
value: $guestCount, in: 0...1000)
}
}
}
.navigationTitle("Novo Evento")
.toolbar {
Button("Salvar") {
saveEvent()
dismiss()
}
}
}
}
private func saveEvent() {
let event: Event
if #available(iOS 26, *) {
switch eventType {
case 1:
event = WorkEvent(
title: title, location: location,
scheduledDate: date, duration: 3600,
budget: budget, departmentCode: departmentCode
)
case 2:
event = SocialEvent(
title: title, location: location,
scheduledDate: date, duration: 3600,
category: .party, guestCount: guestCount
)
default:
event = Event(
title: title, location: location,
scheduledDate: date, duration: 3600
)
}
} else {
event = Event(
title: title, location: location,
scheduledDate: date, duration: 3600
)
}
context.insert(event)
}
}
Esse exemplo mostra como usar verificações de disponibilidade (if #available(iOS 26, *)) pra manter compatibilidade com versões anteriores do iOS enquanto aproveita herança no iOS 26+. É o tipo de padrão que você vai acabar usando bastante na transição.
Perguntas Frequentes
Posso usar herança de modelos no SwiftData com versões anteriores ao iOS 26?
Infelizmente, não. A herança de modelos é exclusiva do iOS 26 e posterior. Se o target mínimo do seu app for iOS 17 ou 18, não vai rolar usar subclasses nos modelos SwiftData. Você vai precisar das anotações @available(iOS 26, *) nas subclasses e verificações de disponibilidade no código que as utiliza.
SwiftData herança funciona com CloudKit e sincronização?
Sim, funciona. Como o SwiftData usa Single Table Inheritance internamente, a sincronização via CloudKit roda normalmente com modelos que usam herança. Os registros de todas as subclasses ficam na mesma tabela, e o CloudKit sincroniza tudo junto. Só fique atento ao HistoryDescriptor pra rastrear mudanças remotas em tipos específicos da hierarquia.
Qual a diferença entre usar herança e composição no SwiftData?
Herança (relação "é-um") funciona melhor quando tipos compartilham muitas propriedades e você precisa de queries polimórficas — buscar todos os eventos independente do tipo, por exemplo. Composição (relação "tem-um") é a escolha certa quando objetos contêm outros objetos sem formar uma hierarquia natural — tipo um Event que tem um Venue. A regra prática: se o type casting faz sentido semântico, herança; se os objetos são independentes, composição.
A herança de modelos impacta a performance do app?
Depende. O SwiftData usa Single Table Inheritance, onde todos os tipos ficam numa tabela SQLite só. Pra apps com dezenas de milhares de registros e poucas subclasses, o impacto é praticamente zero. O problema aparece com muitas subclasses que têm atributos bem diferentes entre si, o que gera uma tabela "larga" com muitos NULLs. Quando isso acontecer, use propertiesToFetch e fetchLimit pra manter tudo sob controle.
Preciso registrar todas as subclasses no ModelContainer?
Sim, sem exceção. Além da classe base, cada subclasse deve ser incluída no modelContainer(for:). Se esquecer de registrar alguma, o SwiftData não vai reconhecê-la e você vai ter erros na persistência. Esse é, de longe, o erro mais comum que vejo quando alguém começa a usar herança no SwiftData.