SwiftData no iOS 27: Observation e History
O SwiftData chegou no iOS 17 com duas formas de observar seus dados: @Query dentro de uma view SwiftUI, ou o encanamento manual de notificações em ModelContext para todo o resto. Nenhuma das duas cobria o caso que mais importa para um app sincronizado, que é saber quando outro dispositivo alterou o store. O iOS 27 fecha as duas lacunas em um único lançamento. O ResultsObserver torna o rastreamento de mudanças um objeto de primeira classe que você pode manter fora de uma view, e o HistoryObserver observa o histórico persistente do SwiftData e incrementa um contador observável quando novas transações chegam, para que seu código de sincronização possa puxar apenas as mudanças mais recentes. O iOS 27 adiciona observação como uma primitiva, não como um efeito colateral do SwiftUI.1
O enquadramento combina com o restante deste cluster: o SwiftData era barato de começar e caro de coordenar. Observar um fetch fora da hierarquia de views significava reconstruir o que o @Query faz na mão. Manter um store em sincronia com um servidor externo, ou reagir a escritas de uma extensão do app, significava percorrer o histórico persistente e reconciliar transações você mesmo. O iOS 27 dá às duas tarefas um tipo nomeado que está em conformidade com Observable, então a mesma maquinaria de atualização do SwiftUI que já move suas views também move sua camada de sincronização.
TL;DR / Principais Conclusões
- O
ResultsObserverobserva e rastreia mudanças em uma coleção de modelos persistentes em um model context, fornecendo atualizações em tempo real quando os dados subjacentes mudam. Ele éObservable, então uma view SwiftUI atualiza automaticamente, e funciona fora de uma view onde o@Querynão alcança.2 - O
HistoryObserverobserva o histórico persistente do SwiftData e expõe uma única propriedade observável,eventCounter, que incrementa quando novas transações chegam. Você filtra por tipo de modelo e autor da transação, depois chamaModelContext.fetchHistorypara ler as mudanças. É a resposta estruturada para sincronizar com um servidor externo ou reagir a escritas de extensões do app.3 - O
@Attribute(.codable)usa a representação codificável da propriedade para armazená-la, dando a você uma forma declarativa de persistir um tipo de valorCodablesem umValueTransformer.4 - Todos os três chegam em iOS, iPadOS, macOS, Mac Catalyst, tvOS, visionOS e watchOS na beta 27.0.234
Observando um fetch fora da view: ResultsObserver
O @Query é excelente e restrito. Ele vive em uma view SwiftUI, é executado novamente quando seu predicate muda, e entrega à view um array. A restrição é a localização. Um view model, um coordenador de sincronização, uma tarefa de exportação ou um reconciliador em segundo plano têm dados que mudam e nenhum @Query para se apoiar. Antes do iOS 27, esses chamadores se inscreviam em notificações de save do ModelContext e refaziam o fetch na mão, que é a reconstrução manual exatamente daquilo que o @Query já faz internamente.
O ResultsObserver é a resposta nomeada do iOS 27. A declaração diz o que ele é:2
final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable
A classe monitora automaticamente mudanças em modelos que correspondem a critérios de fetch especificados e mantém uma coleção de resultados obtidos, o que a torna a ferramenta para manter qualquer consumidor sincronizado com dados persistentes, não apenas uma view.2 Você a configura de duas formas: com um FetchDescriptor completo, ou com predicates de filtro e sort descriptors individuais.2 Os dois caminhos de SectionName importam para listas agrupadas; quando você não precisa de seccionamento, passa Never como o parâmetro de tipo SectionName.2
O ganho sobre a antiga abordagem manual é a conformidade com Observable. O ResultsObserver é Observable, o que permite que views SwiftUI atualizem automaticamente quando os resultados mudam, da mesma forma que objetos de modelo @Observable movem a invalidação de view (coberto em funcionamento interno do @Observable).2 Um coordenador de sincronização que mantém um ResultsObserver recebe notificações de mudança sem escrever uma única linha de NotificationCenter, e qualquer view que lê os resultados do observer é re-renderizada de graça.
Na sessão 274, a Apple introduz o ResultsObserver para o caso que o @Query não consegue atender, buscando e observando o store de qualquer lugar do seu app através do Swift Observation, incluindo um state object ou um jogo que nunca toca em SwiftUI.5
ResultsObserver traz observação no estilo query para código fora das views SwiftUI.
import SwiftData
import Observation
@Observable
final class ShoppingListModel {
let observer: ResultsObserver<ShoppingItem, Never>
init(context: ModelContext) {
let descriptor = FetchDescriptor<ShoppingItem>(
sortBy: [SortDescriptor(\.sortOrder)]
)
// No sectioning, so SectionName is Never.
observer = ResultsObserver(context: context, fetchDescriptor: descriptor)
}
}
A referência publicada pela Apple confirma as declarações de classe e a superfície de configuração (um FetchDescriptor ou predicates de filtro e sort descriptors, Never para o nome da seção quando você não secciona), mas omite as assinaturas exatas dos inicializadores no momento da escrita, então trate os formatos de chamada nestes exemplos como ilustrativos e confirme os rótulos dos parâmetros contra o SDK.
A mudança mental é que o fetch se torna um objeto que você possui e passa adiante, em vez de um property wrapper preso no body de uma view. Um @Query responde “o que esta view exibe?” Um ResultsObserver responde “qual é o estado atual deste fetch, onde quer que eu o esteja mantendo?” A segunda pergunta é a que um chamador que não é view realmente faz.
Reagindo a mudanças de histórico: HistoryObserver
A lacuna que tanto o @Query quanto o ResultsObserver deixam aberta é a mudança que se origina fora do seu fetch em processo. Toda vez que seu store é salvo, o SwiftData registra uma transação de histórico descrevendo o que mudou, de onde veio a mudança, e um token que a identifica. Manter um store em sincronia com um servidor externo, ou reagir a escritas de uma extensão do app, significava percorrer esse histórico persistente você mesmo. O iOS 27 dá à tarefa um observer nomeado.3
O HistoryObserver é a resposta estruturada:3
final class HistoryObserver
O observer observa o histórico persistente do SwiftData e permite que seu código reaja quando novas transações são adicionadas.3 Se você só precisa de certos tipos de mudanças, filtra por tipo de modelo e autor da transação, para reagir às escritas que importam em vez de cada mutação do store.3
Toda a superfície é uma propriedade observável, eventCounter. Quando novas transações chegam no histórico persistente, o contador incrementa; você o observa, e a cada incremento chama a API ModelContext.fetchHistory para ler apenas as mudanças mais recentes.3 Os tokens de histórico ficam nas próprias transações, então fetchHistory retorna apenas o que é novo em vez de reescanear o store. Isso mantém um handler de sincronização rápido conforme o histórico cresce.
import SwiftData
import Observation
// Illustrative call shapes; confirm parameter labels against the SDK.
let historyObserver = HistoryObserver(container: modelContainer, authors: "App")
// Observe eventCounter; on each increment, fetch and process the new history.
let token = withContinuousObservation(of: historyObserver.eventCounter) {
Task { await processChanges() }
}
Filtrar por autor da transação é o detalhe que torna a sincronização com servidor correta. No exemplo da Apple, o observer passa "App" como autor para reagir apenas às mudanças que o app fez, e não repetir mudanças que vieram do servidor de volta para o servidor.3 A cada incremento, seu passo processChanges chama ModelContext.fetchHistory para ler as novas transações e fazer o upload delas. Os padrões de múltiplos processos e sincronização externa que os apps deste cluster resolveram com encanamento de histórico feito à mão (veja disciplina de schema no SwiftData) ganham uma costura do framework onde se apoiar.
Leituras leves que se combinam com o histórico
Uma vez que o HistoryObserver acorda seu handler de sincronização, a próxima pergunta é quanto trabalho fazer. Um handler ingênuo refaz o fetch dos modelos afetados a cada incremento, o que hidrata objetos de modelo completos que você pode não precisar. O SwiftData oferece a você duas leituras que respondem perguntas mais estreitas sem materializar nada, e ambas se combinam naturalmente com a observação de histórico.
O ModelContext.fetchCount(_:) retorna o número de modelos que correspondem a um fetch descriptor como um simples Int, sem carregar os objetos correspondentes:6
func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel
O ModelContext.fetchIdentifiers(_:) retorna as correspondências como [PersistentIdentifier], novamente sem carregar os modelos por trás delas. Uma sobrecarga com batchSize transmite esses identificadores em pedaços para conjuntos de resultados grandes:6
func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel
O SwiftData Group Lab sugeriu um padrão que liga esses recursos diretamente à observação de histórico. Quando uma mudança de histórico chega, busque os identificadores afetados e compare-os com o que a view realmente exibe antes de decidir se deve recarregar, para que você evite hidratar objetos de que não precisa.6 Uma mudança em uma linha que o usuário não está olhando não move nenhum pixel, e uma comparação de identificadores diz isso a você pelo custo de uma busca de chave em vez de um fetch completo. O fetchCount responde à pergunta ainda mais barata de se algo correspondeu de fato, o que basta para decidir se um badge ou um estado vazio precisa mudar.
Persistindo um tipo de valor de forma limpa: @Attribute(.codable)
A terceira adição é pequena e prática. O SwiftData armazena nativamente os tipos primitivos do Swift e relacionamentos @Model, mas uma propriedade cujo tipo é um valor Codable customizado (um struct contendo alguns campos, um enum com valores associados) precisava de um ValueTransformer e do atributo .transformable(by:), que é cerimônia do Core Data vazando de volta pela superfície da macro.
O iOS 27 adiciona a opção de armazenamento codable:4
static var codable: Schema.Attribute.Option { get }
A opção usa a representação codificável da propriedade para armazená-la, então um tipo de valor Codable persiste através de sua própria conformidade Encodable/Decodable sem nenhum transformer para registrar.4 Você a aplica da mesma forma que qualquer outra opção de atributo:
import SwiftData
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}
@Model
final class Place {
var name: String
// Persisted via Coordinate's own Codable conformance.
@Attribute(.codable) var location: Coordinate
}
A regra prática é recorrer a .codable quando uma propriedade é um valor Codable autocontido que não merece sua própria tabela @Model. Um par de coordenadas, um pequeno struct de configurações, um enum com payload: estes são dados, não entidades, e o .codable os armazena inline através da representação que eles já definem, em vez de forçar um transformer ou um relacionamento artificial.
A Apple é explícita sobre os trade-offs. O conteúdo de um atributo codificável é opaco para o SwiftData, então você não pode usá-lo em predicates para filtrar resultados nem em sort descriptors, e uma mudança no formato do tipo codificável (adicionar ou remover propriedades) não vai disparar uma migração, então sua implementação Codable precisa permanecer compatível para frente e para trás.5 A Apple enquadra o .codable como uma escotilha de escape para tipos que você não possui; para tipos que você define, modelá-los como modelos SwiftData ou tipos de valor suportados mantém a ordenação, a filtragem e a indexação na tabela.5
Quando recorrer a cada um
As três adições respondem a três perguntas diferentes, e a pergunta diz a você qual usar.
- Recorra ao
ResultsObserverquando um chamador que não é view precisa de um fetch ao vivo. Um view model, um coordenador, uma tarefa de exportação, qualquer coisa que tenha dados que mudam e não seja umbodySwiftUI. Dentro de uma view, o@Queryainda é a ferramenta mais leve; o observer conquista seu lugar no momento em que o consumidor não é uma view.2 - Recorra ao
HistoryObserverquando você sincroniza o store com algo fora do seu app. Um servidor externo, ou uma extensão do app escrevendo no mesmo store. Observe oeventCounter, filtre por tipo de modelo e autor da transação, e chameModelContext.fetchHistorya cada incremento para ler apenas as novas transações.3 - Recorra ao
@Attribute(.codable)quando uma propriedade é um valorCodable, não uma entidade. Pequenos structs e enums que viajam com seu dono. Se o tipo precisa de sua própria identidade, relacionamentos ou queries, ele quer@Model; se é apenas dado inline, o.codablepula o transformer.4
Os dois observers se compõem. Um ResultsObserver mantém seu fetch em processo ao vivo; um HistoryObserver diz a você quando um push remoto justifica agir sobre o que mudou. Um app fazendo sincronização real entre múltiplos dispositivos usa ambos, e usa .codable para manter suas colunas de tipo de valor honestas ao longo do caminho.
FAQ
Como o ResultsObserver é diferente do @Query?
O @Query é um property wrapper do SwiftUI que vive dentro de uma view e alimenta essa view com um array. O ResultsObserver é uma classe autônoma que você cria e mantém em qualquer lugar, inclusive fora da hierarquia de views, que observa e rastreia mudanças em uma coleção de modelos persistentes em um model context.2 Como o observer é Observable, uma view SwiftUI que o lê ainda atualiza automaticamente, então ele cobre tanto o caso dentro da view quanto o caso de view model ou coordenador que o @Query não alcança.2
O que o HistoryObserver realmente observa?
Ele observa o histórico persistente do SwiftData, o registro de transações que o SwiftData escreve toda vez que o store é salvo.3 Ele expõe uma única propriedade observável, eventCounter, que incrementa quando novas transações estão disponíveis; você pode filtrar por tipo de modelo e autor da transação para que apenas as mudanças que importam movam o contador.3 A cada incremento, seu código chama a API ModelContext.fetchHistory para ler as novas transações, o que o torna o handler estruturado para sincronizar com um servidor externo ou reagir a escritas de extensões do app.3
Posso usar ResultsObserver e HistoryObserver juntos?
Sim, e um app sincronizado geralmente deveria. O ResultsObserver mantém um fetch em processo atualizado conforme o context local muda; o HistoryObserver traz à tona mudanças registradas no histórico persistente, filtradas por tipo de modelo e autor da transação.23 Ambos são objetos observáveis aos quais você pode reagir a partir de uma view SwiftUI ou de outro observer, então eles se encaixam no mesmo fluxo reativo sem tratamento de notificações separado.23
Quando devo usar @Attribute(.codable) em vez de um relacionamento?
Use .codable quando a propriedade é um tipo de valor Codable autocontido que não tem identidade independente, já que a opção armazena a propriedade através de sua própria representação codificável.4 Use um relacionamento @Model quando o valor é uma entidade real com seu próprio ciclo de vida, identidade ou queries. A linha divisória é se a coisa é um dado que pertence ao seu dono, ou uma entidade que outras linhas referenciam.
O cluster completo do Apple Ecosystem: disciplina de schema no SwiftData para o custo de migração sobre o qual a observação se assenta; o guia de migrações do SwiftData para a maquinaria de VersionedSchema e MigrationPlan; funcionamento interno do @Observable para o modelo de observação ao qual essas classes se conectam; funcionamento interno do SwiftUI para o substrato do framework por baixo. O hub está na Série Apple Ecosystem. Para um contexto mais amplo de iOS com agentes de IA, veja o guia de Desenvolvimento de Agentes em iOS.
Referências
-
Documentação para Desenvolvedores da Apple: SwiftData. A referência do framework cobrindo
@Model,ModelContext,ModelContainer, queries e as adições de observação do iOS 27. ↩ -
Documentação para Desenvolvedores da Apple:
ResultsObserver(beta do iOS 27.0). “Observes and tracks changes to a collection of persistent models in a model context.” Declarada comofinal class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable; configurável com umFetchDescriptorou com predicates de filtro e sort descriptors;Observable, então views SwiftUI atualizam automaticamente; passeNevercomoSectionNamequando nenhum seccionamento for necessário. ↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, sessão 274 da WWDC26, What’s new in SwiftData, e Documentação para Desenvolvedores da Apple:
HistoryObserver(beta do iOS 27.0). Declarada comofinal class HistoryObserver. Conforme a sessão 274, ela observa o histórico persistente do SwiftData e “has a single observable property” (eventCounter); “when new transactions are available in the persistent history, the eventCounter increments”, e “your code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes.” Ela “lets you filter by model type and transaction author”; o exemplo da sessão passa"App"como o autor para que mudanças originadas no app não sejam repetidas de volta para um servidor externo. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Documentação para Desenvolvedores da Apple:
codable(beta do iOS 27.0). “Uses the property’s codable representation to store the property.” Declarada comostatic var codable: Schema.Attribute.Option { get }. ↩↩↩↩↩↩ -
Apple, sessão 274 da WWDC26, What’s new in SwiftData. A Apple introduz o
ResultsObserver, que “fetches data from your SwiftData store and then observes your store for changes”, mas “works anywhere in your app (independent of SwiftUI views) using Swift Observation”, e nomeia um state object ou um jogo escrito em SceneKit como casos que o@Querynão alcança. ↩↩↩ -
Documentação para Desenvolvedores da Apple:
fetchCount(_:)efetchIdentifiers(_:)emModelContext. OfetchCounté declarado comofunc fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModele “returns the number of models that match the criteria of the specified fetch descriptor.” OfetchIdentifiersé declarado comofunc fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModele retorna “an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria”; uma sobrecargafetchIdentifiers(_:batchSize:)retorna os identificadores em lotes como umFetchResultsCollection<PersistentIdentifier>. A fonte do padrão de buscar-identificadores-afetados-e-então-comparar-com-a-view: Parafraseado de uma gravação transcrita localmente do SwiftData Group Lab da WWDC 2026; a Apple não publica legendas oficiais para os labs. ↩↩↩