← すべての記事

iOS 27のSwiftData:ObservationとHistory

SwiftDataはiOS 17で、データを監視する2つの方法とともに登場しました。SwiftUIビュー内で使う@Queryと、それ以外のあらゆる場面でModelContextに手作業で通知を組み込む方法です。しかしどちらも、同期アプリにとって最も重要なケース、つまり別のデバイスがストアを変更したことを知るというケースをカバーしていませんでした。iOS 27は、この2つのギャップを1つのリリースで埋めます。ResultsObserverは、変更追跡をビューの外で保持できる第一級のオブジェクトにします。そしてHistoryObserverはSwiftDataの永続履歴を監視し、新しいトランザクションが届くたびにobservableなカウンターをインクリメントするため、同期コードは最新の変更だけを取得できます。iOS 27はObservationを、SwiftUIの副作用ではなくプリミティブとして追加するのです。1

この捉え方は、このクラスタの他の記事と一致します。SwiftDataは始めるのは安く、調整するのは高くつくものでした。ビュー階層の外でフェッチを監視するには、@Queryが行っていることを手作業で再構築する必要がありました。ストアを外部サーバーと同期させたり、App Extensionからの書き込みに反応したりするには、永続履歴を自分で辿ってトランザクションを突き合わせなければなりませんでした。iOS 27は、この両方の仕事にObservableへ準拠した名前付きの型を与えます。すでにビューを駆動しているのと同じSwiftUIの更新機構が、同期レイヤーも駆動するようになるのです。

TL;DR / 要点

  • ResultsObserverは、model context内の永続モデルのコレクションへの変更を監視・追跡し、基盤となるデータが変化したときにリアルタイムの更新を提供します。Observableなので、SwiftUIビューは自動的に更新され、@Queryでは届かないビューの外でも機能します。2
  • HistoryObserverはSwiftDataの永続履歴を監視し、新しいトランザクションが届いたときにインクリメントする単一のobservableプロパティeventCounterを公開します。モデルの型とトランザクションのauthorでフィルタリングしてから、ModelContext.fetchHistoryを呼び出して変更を読み取ります。外部サーバーとの同期や、App Extensionの書き込みへの反応に対する、構造化された答えです。3
  • @Attribute(.codable)はプロパティのcodable表現を使ってプロパティを格納し、ValueTransformerなしでCodableな値型を永続化する宣言的な方法を提供します。4
  • 3つすべてが27.0ベータにおいて、iOS、iPadOS、macOS、Mac Catalyst、tvOS、visionOS、watchOSで提供されます。234

ビューの外でフェッチを監視する:ResultsObserver

@Queryは優れていますが、制約があります。SwiftUIビューの中に存在し、predicateが変わると再実行され、ビューに配列を渡します。その制約は「場所」です。view model、同期コーディネーター、エクスポートジョブ、バックグラウンドの整合処理は、変化するデータを持っていても、頼れる@Queryを持っていません。iOS 27以前、こうした呼び出し側はModelContextの保存通知を購読し、手作業で再フェッチしていました。これはまさに@Queryが内部で行っていることを、手で再構築したものにほかなりません。

ResultsObserverは、iOS 27の名前付きの答えです。その宣言は、それが何であるかを示しています。2

final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable

このクラスは、指定されたフェッチ条件に一致するモデルへの変更を自動的に監視し、フェッチ結果のコレクションを維持します。そのため、これはビューだけでなく、あらゆる利用側を永続データと同期させ続けるためのツールとなります。2 設定方法は2通りあります。完全なFetchDescriptorを使う方法と、個々のフィルターpredicateとソート記述子を使う方法です。2 SectionNameの2つの経路は、グループ化されたリストにおいて意味を持ちます。セクション分けが不要な場合は、SectionName型パラメータとしてNeverを渡します。2

従来の手作業のアプローチに対する見返りは、Observableへの準拠です。ResultsObserverObservableであり、結果が変化したときにSwiftUIビューを自動的に更新できます。これは@Observableなモデルオブジェクトがビューの無効化を駆動するのと同じ仕組みです(@Observableの内部構造で解説しています)。2 ResultsObserverを保持する同期コーディネーターは、NotificationCenterの行を1行も書くことなく変更通知を受け取ります。そしてオブザーバーの結果を読むビューはすべて、ただで再描画されます。

セッション274でAppleは、@Queryでは対応できないケースのためにResultsObserverを導入しています。Swift Observationを通じて、アプリのどこからでもストアをフェッチして監視できるのです。state objectや、SwiftUIに一切触れないゲームも含めて。5

Watch on Apple Developer ↗
ResultsObserverは、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)
    }
}

Appleの公開リファレンスは、クラス宣言と設定の入り口(FetchDescriptor、またはフィルターpredicateとソート記述子、セクション分けしない場合のセクション名としてのNever)を確認していますが、執筆時点では正確なイニシャライザのシグネチャを省略しています。そのため、これらの例における呼び出しの形は説明用のものとして扱い、パラメータラベルはSDKと照らし合わせて確認してください。

発想の転換は、フェッチがビューのbodyに閉じ込められたプロパティラッパーではなく、自分が所有して持ち回るオブジェクトになる、という点にあります。@Queryは「このビューは何を表示するのか?」に答えます。ResultsObserverは「私がこのフェッチをどこで保持していても、その現在の状態は何か?」に答えます。後者こそ、ビューではない呼び出し側が実際に問う問いなのです。

履歴の変更に反応する:HistoryObserver

@QueryResultsObserverの両方が残すギャップは、プロセス内のフェッチの外側から生じる変更です。ストアが保存されるたびに、SwiftDataは何が変わったか、変更がどこから来たか、そしてそれを識別するトークンを記述した履歴トランザクションを記録します。ストアを外部サーバーと同期させたり、App Extensionからの書き込みに反応したりするには、その永続履歴を自分で辿る必要がありました。iOS 27は、この仕事に名前付きのオブザーバーを与えます。3

HistoryObserverが、その構造化された答えです。3

final class HistoryObserver

このオブザーバーはSwiftDataの永続履歴を監視し、新しいトランザクションが追加されたときにコードが反応できるようにします。3 特定の種類の変更だけが必要な場合は、モデルの型とトランザクションのauthorでフィルタリングできます。そうすれば、すべてのストア変更ではなく、自分が気にする書き込みにだけ反応できます。3

その全体は、ひとつのobservableプロパティeventCounterに集約されます。新しいトランザクションが永続履歴に届くと、カウンターがインクリメントされます。これを監視し、インクリメントのたびにModelContext.fetchHistory APIを呼び出して、最新の変更だけを読み取ります。3 履歴トークンはトランザクション自体に存在するため、fetchHistoryはストア全体を再スキャンするのではなく、新しいものだけを返します。これにより、履歴が増えても同期ハンドラは速いままです。

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() }
}

トランザクションのauthorでフィルタリングすることが、サーバー同期を正しくする要となるディテールです。Appleの例では、オブザーバーはauthorとして"App"を渡し、アプリが行った変更にだけ反応するようにしています。そしてサーバーから来た変更をサーバーへ再生し返すことはしません。3 インクリメントのたびに、processChangesのステップがModelContext.fetchHistoryを呼び出して新しいトランザクションを読み取り、アップロードします。このクラスタのアプリが手書きの履歴処理で解決していたマルチプロセスや外部同期のパターン(SwiftDataのスキーマ規律を参照)に、頼れるフレームワークの継ぎ目ができるのです。

履歴と組み合わせる軽量な読み取り

HistoryObserverが同期ハンドラを起こしたら、次の問いは「どれだけの作業をすべきか」です。素朴なハンドラはインクリメントのたびに該当モデルを再フェッチし、必要のないかもしれない完全なモデルオブジェクトを生成してしまいます。SwiftDataは、何も実体化することなく、より絞り込んだ問いに答える2つの読み取りを用意しており、どちらも履歴監視と自然に組み合わさります。

ModelContext.fetchCount(_:)は、一致するオブジェクトを読み込むことなく、フェッチ記述子に一致するモデルの数を素のIntとして返します。6

func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel

ModelContext.fetchIdentifiers(_:)は、これもまた背後のモデルを読み込むことなく、一致したものを[PersistentIdentifier]として返します。batchSizeのオーバーロードは、大きな結果セットのためにそれらの識別子をチャンク単位でストリーミングします。6

func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel

SwiftData Group Labは、これらを履歴監視に直接結びつけるパターンを提案しました。履歴の変更が届いたら、該当する識別子をフェッチし、再読み込みするかどうかを決める前に、ビューが実際に表示しているものと比較します。そうすれば、必要のないオブジェクトの生成を避けられます。6 ユーザーが見ていない行への変更はピクセルを1つも動かしません。そして識別子の比較は、完全なフェッチではなくキー照合のコストで、それを教えてくれます。fetchCountはさらに安く、そもそも何かが一致したかどうかという問いに答えます。これは、バッジや空状態の表示を切り替える必要があるかを判断するのに十分です。

値型をきれいに永続化する:@Attribute(.codable)

3つ目の追加は小さく、実用的です。SwiftDataはSwiftのプリミティブ型と@Modelのリレーションシップをネイティブに格納しますが、型がカスタムのCodableな値(数フィールドを持つ構造体や、関連値を持つenum)であるプロパティには、ValueTransformer.transformable(by:)属性が必要でした。これはCore Dataの儀式がマクロの表層から漏れ戻ってきたものです。

iOS 27は、codableストレージオプションを追加します。4

static var codable: Schema.Attribute.Option { get }

このオプションはプロパティのcodable表現を使ってプロパティを格納します。そのためCodableな値型は、登録すべきtransformerなしに、自身のEncodable/Decodable準拠を通じて永続化されます。4 適用方法は、他のあらゆる属性オプションと同じです。

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
}

実用的な指針は、プロパティが自己完結したCodableな値で、独自の@Modelテーブルに値しない場合に.codableを選ぶことです。座標のペア、小さな設定構造体、ペイロードを持つenum。これらはエンティティではなくデータであり、.codableはtransformerや不自然なリレーションシップを強いることなく、それらがすでに定義している表現を通じてインラインで格納します。

Appleはトレードオフについて明確です。codable属性の内容はSwiftDataにとって不透明です。そのため、結果のフィルタリングのためにpredicateで使ったり、ソート記述子で使ったりすることはできません。また、codableな型の形の変更(プロパティの追加や削除)はマイグレーションを起こさないため、そのCodable実装は前方互換・後方互換を保たなければなりません。5 Appleは.codableを、自分が所有しない型のための脱出口として位置づけています。自分が定義する型については、SwiftDataのモデルやサポートされた値型としてモデリングすることで、ソート・フィルタリング・インデックス作成をテーブル上に保てます。5

それぞれをいつ選ぶか

3つの追加は、3つの異なる問いに答えます。そして問いそのものが、どれを使うかを教えてくれます。

  • ビューではない呼び出し側がライブなフェッチを必要とするとき、ResultsObserverを選びます。 view model、コーディネーター、エクスポートタスク、つまりSwiftUIのbodyではないのに変化するデータを持つあらゆるものです。ビューの中では、@Queryが依然として軽いツールです。利用側がビューでなくなった瞬間に、オブザーバーがその居場所を得ます。2
  • アプリの外にある何かとストアを同期するとき、HistoryObserverを選びます。 外部サーバーや、同じストアに書き込むApp Extensionです。eventCounterを監視し、モデルの型とトランザクションのauthorでフィルタリングし、インクリメントのたびにModelContext.fetchHistoryを呼び出して新しいトランザクションだけを読み取ります。3
  • プロパティがエンティティではなくCodableな値であるとき、@Attribute(.codable)を選びます。 所有者とともに移動する小さな構造体やenumです。型が独自のアイデンティティ、リレーションシップ、クエリを必要とするなら、それは@Modelを求めています。単なるインラインのデータであれば、.codableがtransformerを省きます。4

2つのオブザーバーは組み合わせられます。ResultsObserverがプロセス内のフェッチをライブに保ち、HistoryObserverがリモートのプッシュに対して変更へ反応すべきタイミングを教えます。本当のマルチデバイス同期を行うアプリは両方を使い、その過程で.codableを使って値型のカラムを誠実に保ちます。

FAQ

ResultsObserver@Queryとどう違うのですか?

@Queryはビューの中に存在し、そのビューに配列を渡すSwiftUIのプロパティラッパーです。ResultsObserverは、ビュー階層の外を含めどこでも作成・保持できるスタンドアロンのクラスで、model context内の永続モデルのコレクションへの変更を監視・追跡します。2 オブザーバーはObservableなので、それを読むSwiftUIビューは依然として自動的に更新されます。つまり、ビュー内のケースと、@Queryが届かないview modelやコーディネーターのケースの両方をカバーするのです。2

HistoryObserverは実際に何を監視するのですか?

SwiftDataの永続履歴を監視します。これはストアが保存されるたびにSwiftDataが書き込むトランザクションの記録です。3 単一のobservableプロパティeventCounterを公開し、新しいトランザクションが利用可能になるとインクリメントされます。モデルの型とトランザクションのauthorでフィルタリングできるので、自分が気にする変更だけがカウンターを動かします。3 インクリメントのたびに、コードはModelContext.fetchHistory APIを呼び出して新しいトランザクションを読み取ります。これが、外部サーバーとの同期や、App Extensionの書き込みへの反応に対する構造化されたハンドラとなるのです。3

ResultsObserverHistoryObserverを一緒に使えますか?

はい。同期アプリでは、たいていそうすべきです。ResultsObserverはローカルのコンテキストが変化するにつれてプロセス内のフェッチを最新に保ちます。HistoryObserverは永続履歴に記録された変更を、モデルの型とトランザクションのauthorでフィルタリングして浮かび上がらせます。23 どちらもobservableなオブジェクトで、SwiftUIビューや別のオブザーバーから反応できるため、別個の通知処理なしに同じリアクティブな流れに組み込まれます。23

リレーションシップの代わりに@Attribute(.codable)をいつ使うべきですか?

プロパティが独立したアイデンティティを持たない自己完結したCodableな値型であるときに.codableを使います。このオプションは、プロパティ自身のcodable表現を通じてプロパティを格納するからです。4 値が独自のライフサイクル、アイデンティティ、クエリを持つ本物のエンティティであるときは、@Modelのリレーションシップを使います。分かれ目は、それが所有者に属するデータなのか、それとも他の行が参照するエンティティなのか、という点です。

Apple Ecosystemクラスタの全体は次のとおりです。Observationが上に載るマイグレーションコストについてはSwiftDataのスキーマ規律VersionedSchemaMigrationPlanの機構についてはSwiftDataマイグレーションガイド、これらのクラスが接続するObservationモデルについては@Observableの内部構造、その下にあるフレームワークの基盤についてはSwiftUIの内部構造を参照してください。ハブはApple Ecosystemシリーズにあります。AIエージェントを伴うiOSのより広い文脈については、iOS Agent Development guideをご覧ください。

References


  1. Apple Developer Documentation: SwiftData. The framework reference covering @Model, ModelContext, ModelContainer, queries, and the iOS 27 observation additions. 

  2. Apple Developer Documentation: ResultsObserver (iOS 27.0 beta). “Observes and tracks changes to a collection of persistent models in a model context.” Declared as final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable; configurable with a FetchDescriptor or with filter predicates and sort descriptors; Observable, so SwiftUI views update automatically; pass Never as SectionName when no sectioning is needed. 

  3. Apple, WWDC26 session 274, What’s new in SwiftData, and Apple Developer Documentation: HistoryObserver (iOS 27.0 beta). Declared as final class HistoryObserver. Per session 274, it observes SwiftData’s persistent history and “has a single observable property” (eventCounter); “when new transactions are available in the persistent history, the eventCounter increments,” and “your code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes.” It “lets you filter by model type and transaction author”; the session’s example passes "App" as the author so app-originated changes are not replayed back to an external server. 

  4. Apple Developer Documentation: codable (iOS 27.0 beta). “Uses the property’s codable representation to store the property.” Declared as static var codable: Schema.Attribute.Option { get }

  5. Apple, WWDC26 session 274, What’s new in SwiftData. Apple introduces ResultsObserver, which “fetches data from your SwiftData store and then observes your store for changes” but “works anywhere in your app (independent of SwiftUI views) using Swift Observation,” and names a state object or a game written in SceneKit as cases @Query cannot reach. 

  6. Apple Developer Documentation: fetchCount(_:) and fetchIdentifiers(_:) on ModelContext. fetchCount is declared func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel and “returns the number of models that match the criteria of the specified fetch descriptor.” fetchIdentifiers is declared func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel and returns “an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria”; a fetchIdentifiers(_:batchSize:) overload returns the identifiers in batches as a FetchResultsCollection<PersistentIdentifier>. The fetch-affected-identifiers-then-compare-against-the-view pattern source: Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftData Group Lab; Apple publishes no official captions for the labs. 

関連記事

SwiftData マイグレーション:lightweight か custom か、そして V2 が不要なケース

SwiftData のマイグレーションモデルは VersionedSchema、MigrationStage、SchemaMigrationPlan で構成されます。ほとんどのスキーマ変更に V2 スキーマは不要ですが、必要なケースは確かに…

4 分で読める

SwiftDataの真のコストはスキーマの規律にある

SwiftDataのAPIは2つのマクロだけです。コストはリリース後に発生します。オプショナルなフィールドは安価なマイグレーションですが、非オプショナルな追加にはVersionedSchemaが必要です。

3 分で読める

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 分で読める