← Todos os Posts

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 ResultsObserver observa 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 @Query não alcança.2
  • O HistoryObserver observa 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 chama ModelContext.fetchHistory para 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 valor Codable sem um ValueTransformer.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

Watch on Apple Developer ↗
O 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 ResultsObserver quando 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 um body SwiftUI. Dentro de uma view, o @Query ainda é a ferramenta mais leve; o observer conquista seu lugar no momento em que o consumidor não é uma view.2
  • Recorra ao HistoryObserver quando você sincroniza o store com algo fora do seu app. Um servidor externo, ou uma extensão do app escrevendo no mesmo store. Observe o eventCounter, filtre por tipo de modelo e autor da transação, e chame ModelContext.fetchHistory a cada incremento para ler apenas as novas transações.3
  • Recorra ao @Attribute(.codable) quando uma propriedade é um valor Codable, 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 .codable pula 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


  1. 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. 

  2. 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 como final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable; configurável com um FetchDescriptor ou com predicates de filtro e sort descriptors; Observable, então views SwiftUI atualizam automaticamente; passe Never como SectionName quando nenhum seccionamento for necessário. 

  3. 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 como final 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. 

  4. Documentação para Desenvolvedores da Apple: codable (beta do iOS 27.0). “Uses the property’s codable representation to store the property.” Declarada como static var codable: Schema.Attribute.Option { get }

  5. 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 @Query não alcança. 

  6. Documentação para Desenvolvedores da Apple: fetchCount(_:) e fetchIdentifiers(_:) em ModelContext. O fetchCount é declarado como func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel e “returns the number of models that match the criteria of the specified fetch descriptor.” O fetchIdentifiers é declarado como func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel e retorna “an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria”; uma sobrecarga fetchIdentifiers(_:batchSize:) retorna os identificadores em lotes como um FetchResultsCollection<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. 

Artigos relacionados

Migrações no SwiftData: lightweight vs. custom, e quando você não precisa de um V2

O modelo de migração do SwiftData usa VersionedSchema, MigrationStage e SchemaMigrationPlan. A maioria das mudanças de e…

14 min de leitura

O verdadeiro custo do SwiftData é a disciplina de schema

A API do SwiftData são duas macros. O custo é o que acontece depois que você publica. Campos opcionais são a migração ba…

17 min de leitura

The Robots Are Taking Exams in My Search Console

First-party GSC data: 91% of 3.8M impressions fail a human-query filter. Exam questions, pasted errors, and agent sweeps…

10 min de leitura