every Tech Blog

株式会社エブリーのTech Blogです。

Swift 6.2 @Observableの変更をAsyncSequenceで監視する

この記事は every Tech Blog Advent Calendar 2025 の 24日目の記事です。

はじめに

Swift 5.9で導入された Observation フレームワークは、@Observable マクロを用いた簡潔な記述が可能で、特にSwiftUIのView更新において高いパフォーマンスを発揮します。

一方で、既存の Combine フレームワーク(ObservableObject)からの移行を検討する際、課題となる点がありました。それは ViewModel や Service など、UI以外の場所での値の監視です。Combine では @Published プロパティのProjected Valueを用いて値の変化をストリームとして扱えますが、それと同等の標準的な手段がこれまでの Observation フレームワークには不足していました。

Swift 6.2では、この点が解消されています。@Observable クラスのプロパティの変化を AsyncSequence として監視する機能(Observations)が追加され、Combine に依存することなく、標準APIのみで値の監視が可能になりました。

本記事では、この新しい監視手法について整理します。

ObservableObject による値の監視

Observation フレームワークが登場する以前、SwiftUI では ObservableObject と Combine を用いた状態管理が一般的でした。

@Published プロパティの projected value($)を利用することで、値の変化をストリームとして扱える点は大きな利点でした。

import Combine

class CounterViewModel: ObservableObject {
    @Published var count: Int = 0
    private var cancellables = Set<AnyCancellable>()
}

let viewModel = CounterViewModel()

// $をつけることで Publisher として扱える
viewModel.$count
    .sink { value in
        print("count changed:", value)
    }
    .store(in: &viewModel.cancellables)

この仕組みにより、View の更新だけでなく、ViewModel や Service など UI 以外のレイヤーでも、値の変化を一貫した方法で監視できていました。この点は、ObservableObject が持つ強みの一つです。

Observation フレームワークと withObservationTracking

Swift 5.9 で Observation フレームワークが導入されると、View 更新のパフォーマンスと記述の簡潔さは大きく改善されました。一方で、View 以外で値の変化を検知する方法として利用されているのが withObservationTracking です。

import Observation

@Observable
class Counter {
    var value: Int = 0
}

let counter = Counter()

func observe() {
    withObservationTracking {
        // 1. ここで値を読む(アクセスする)ことで監視対象にする
        print("Current value: \(counter.value)") 
    } onChange: {
        // 2. 変更前(willSetタイミング)に呼ばれる
        print("Value will change...")
        
        Task {
            // 3. 再帰的に監視を続ける
            observe()
        }
    }
}

withObservationTracking は、クロージャ内で読み取られたプロパティへの依存関係を自動的に追跡し、それらが変更された際に onChange を呼び出します。これは SwiftUI の View 更新を支える中核的な仕組みです。

withObservationTracking の課題

しかし、この方法をロジック層で使うには、いくつかの問題がありました。

値そのものを直接受け取れない

onChange で通知されるのは「変更される」という事実のみで、変更後の値は再度読み取る必要があります。

再登録が必要

変更を継続的に監視するためには、onChange の中で再び withObservationTracking を呼び出す再帰的な実装が必要です。

非同期処理との相性

withObservationTracking 自体は同期的であり、Callbackベースの記述になるため、async/await のフローと組み合わせる際に直感的な記述が難しくなります。

Swift 6.2: AsyncSequence による監視の導入

Swift 6.2 では、こうした課題を解消する形で、@Observable クラスのプロパティの変化を AsyncSequence として監視できる Observations 型が追加されました。

import Observation

@Observable
class Counter {
    var value: Int = 0
}

let counter = Counter()

// 監視対象を定義
let counterChanges = Observations {
    counter.value
}

Task {
    for await value in counterChanges {
        print("value changed:", value)
    }
}

withObservationTracking が持っていた課題は解消され、簡潔な記述で安全に値の変化を監視できるようになりました。

Combine フレームワークに依存せず、自然な形でSwiftの並行処理モデルに直接統合されています。

まとめ

Swift 6.2 の Observations 導入により、ObservableObject が持っていた「Combine による値の監視」という優位性は解消されました。

ただし、この機能を利用するためには iOS 26以降の環境が必要となるため、サポートOSの要件によっては、すぐにプロダクションコードで全面的に採用することは難しいかもしれません。

とはいえ、「View 以外での監視」という最大の課題に対する標準的な解法が示された意義は大きく、安心して Observation への移行を進められる環境が整ったと思います。