Observationとは

ObservationとはWWDC23で発表された、Swiftで値の変更を監視するためのフレームワークです。
iOS17以降で使用可能で、従来のCombineを使用した方法に比べて、よりシンプルに値の変更を検知するコードを書けるようになりました。
今回はObservationの基本的な使い方と、値の変更を検知して通知するまでの流れを、内部設計にまで踏み込んで解説していきます。
動作環境
- macOS Tahoe 26.0
- Xcode 26.0 beta
基本的な使い方
withObservationTracking

まずはObservationを使う上で、最も基本的な関数となる withObservationTracking
について解説していきます。
withObservationTracking
は、指定したプロパティの変更を監視し、変更時にコールバック処理を実行する関数です。これを使えば、SwiftUIの外でもObservationの仕組みを活用できます。
import Observation
@Observable
final class Chat {
var message: String
var isRead: Bool
}
var chat = Chat(message: "", isRead: false)
withObservationTracking {
let _ = chat.isRead // isReadのみを参照
} onChange: {
Task { @MainActor in
print("On Changed: \(chat.message) | \(chat.isRead)")
}
}
chat.message = "Hello, World!" // messageは監視対象外なのでonChangeは呼ばれない
chat.isRead = true
chat.isRead = false
// 実行結果
On Changed: Hello, World! | false
このコードのポイントをまとめます。
1. 参照したプロパティのみを監視
withObservationTracking
は、追跡クロージャ(apply: )内で実際に参照されたプロパティのみを監視対象とします。上の例ではchat.isRead
のみを参照しているため、message
プロパティが変更されてもonChange
は呼ばれません。
2. willSetタイミングでの実行
変更通知はwillSet
のタイミングで発生します。そのため、onChange
クロージャ内でプロパティを参照しても、まだ古い値が表示されます。上記の例でisRead
がfalse
のままなのはこのためです。
3. onChangeが呼ばれるのは1回きり
withObservationTracking
による監視は一回限りです。最初の変更が検知され、onChangeが実行されると監視が終了するため、継続的に監視したい場合は再帰的に実装する必要があります。
以下は再帰的に関数を呼ばし、継続的に変更を検知するコード例です。
import Observation
@Observable
final class Chat {
var message: String
var isRead: Bool
}
var chat = Chat(message: "", isRead: false)
@MainActor
func observeIsRead() {
withObservationTracking {
let _ = chat.isRead
} onChange: {
Task { @MainActor in
print("On Changed: \(chat.message) | \(chat.isRead)")
observeIsRead() // 自分を再帰的に呼び出す
}
}
}
Task { @MainActor in
observeIsRead()
chat.message = "Hello, World!"
chat.isRead = true
try? await Task.sleep(for: .seconds(1))
chat.isRead = false
}
SwiftUIでの使用
ObservationはSwiftUIでの使用を前提としたフレームワークです。
従来のCombineを使用する方法と比べて、かなりシンプルにコードを記述できるようになります。
Combineを使った方法
- クラスに
ObservableObject
プロトコルに準拠 - 監視するプロパティに
@Published
を付与 - View側で
@ObservedObject
や@StateObject
を使用
Observationを使った方法
- クラスに
@Observable
を付けるだけ @Published
は不要- View側では通常の
@State
で定義可能
import SwiftUI
import Observation
@Observable
final class Chat {
var message: String
var isRead: Bool
}
struct ContentView: View {
@State var chat = Chat(message: "", isRead: false)
var body: some View {
VStack {
Text("メッセージ: \(chat.message)")
Text("既読済み: \(chat.isRead.description)")
Spacer()
TextField("", text: $chat.message)
.textFieldStyle(.roundedBorder)
Button("既読") {
chat.isRead = true
}
}
.padding()
}
}
おそらくSwiftUIの内部で、自動的にwithObservationTrackingが自動的に補完されているのではないかと思います
struct ContentView: View {
@State var chat = Chat(message: "", isRead: false)
var body: some View {
withObservationTracking { // おそらくこれが自動で補完されている🤔
VStack {
Text("メッセージ: \\(chat.message)")
Text("既読済み: \\(chat.isRead.description)")
Spacer()
TextField("", text: $chat.message)
.textFieldStyle(.roundedBorder)
Button("既読") {
chat.isRead = true
}
}
.padding()
} onChange: {
// Viewを再レンダリング
scheduleViewUpdate()
}
}
}
Observationの仕組み
Observationの核心は以下の2つの機能です。
- 読み取り追跡:どのプロパティが読まれたかを記録
- 変更通知:プロパティが変更されたときに通知
どうやってこの2つの機能を実現しているのでしょうか?
Observableマクロの展開
まずはクラスに付与しているObservableマクロを展開してみます。
展開前
@Observable
final class Chat {
var message: String = ""
}
展開後
@Observable
final class Chat {
@ObservationTracked
var message: String = ""
@ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
internal nonisolated func access<$s17ObservationSample4Chat10ObservablefMm_6MemberfMu_>(
keyPath: KeyPath<Chat, $s17ObservationSample4Chat10ObservablefMm_6MemberfMu_>
) {
_$observationRegistrar.access(self, keyPath: keyPath)
}
internal nonisolated func withMutation<$s17ObservationSample4Chat10ObservablefMm_6MemberfMu0_, $s17ObservationSample4Chat10ObservablefMm_14MutationResultfMu_>(
keyPath: KeyPath<Chat, $s17ObservationSample4Chat10ObservablefMm_6MemberfMu0_>,
_ mutation: () throws -> $s17ObservationSample4Chat10ObservablefMm_14MutationResultfMu_
) rethrows -> $s17ObservationSample4Chat10ObservablefMm_14MutationResultfMu_ {
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
private nonisolated func shouldNotifyObservers<$s17ObservationSample4Chat10ObservablefMm_6MemberfMu1_>(_ lhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu1_, _ rhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu1_) -> Bool {
true
}
private nonisolated func shouldNotifyObservers<$s17ObservationSample4Chat10ObservablefMm_6MemberfMu2_: Equatable>(_ lhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu2_, _ rhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu2_) -> Bool {
lhs != rhs
}
private nonisolated func shouldNotifyObservers<$s17ObservationSample4Chat10ObservablefMm_6MemberfMu3_: AnyObject>(_ lhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu3_, _ rhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu3_) -> Bool {
lhs !== rhs
}
private nonisolated func shouldNotifyObservers<$s17ObservationSample4Chat10ObservablefMm_6MemberfMu4_: Equatable & AnyObject>(_ lhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu4_, _ rhs: $s17ObservationSample4Chat10ObservablefMm_6MemberfMu4_) -> Bool {
lhs != rhs
}
}
extension Chat: Observation.Observable {
}
展開後のコードについてポイントを解説します。
1. ObservationRegistrarの追加
@ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
ObservationRegistrar
のインスタンスが自動的に追加されます。これがObservationシステムの中核となっていて、アクセス履歴の保持も、値の変更による通知も、このクラスを起点に行われます。
2. accessメソッドの追加
internal nonisolated func access<Member>(
keyPath: KeyPath<Chat, Member>
) {
_$observationRegistrar.access(self, keyPath: keyPath)
}
このメソッドは、各プロパティが読み取られるたびに呼び出され、どのプロパティにアクセスされたかを記録します。このおかげでアクセスされたプロパティの変更のみを検知できるようになります。
3. withMutationメソッドの追加
internal nonisolated func withMutation<Member, Result>(
keyPath: KeyPath<Chat, Member>,
_ mutation: () throws -> Result
) rethrows -> Result {
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
このメソッドは、プロパティが変更されるときに呼び出され、変更前後の通知を行います。実際の変更処理をクロージャで受け取り、適切なタイミングで監視者に通知を送信します。
4. 各プロパティに @ObservationTracked
を付与
@ObservationTracked
var message: String = ""
このマクロを展開すると、以下のようになります。
// 展開後
var message: String = ""
{
@storageRestrictions(initializes: _value)
init(initialValue) {
_value = initialValue
}
get {
access(keyPath: \.value) // アクセスを記録
return _value
}
set {
// 変更されたかどうかを検知
// 変更されてなければ通知せず終了
guard shouldNotifyObservers(_value, newValue) else {
_value = newValue
return
}
// 変更を通知
withMutation(keyPath: \.value) {
_value = newValue
}
}
}
アクセスされたら access(keyPath:)
を、値が変更されたら withMutation(keyPath: mutation:)
を呼び出しているのがわかると思います。
アクセス履歴の保持
ではここから、アクセス履歴の保持を行うまでの内部のフローを解説します。
登場するクラスは以下の2つです。
- ObservationRegistrar
- ObservationTracking
まずはwithObservationTrackingの定義を確認します。
public func withObservationTracking<T>(
_ apply: () -> T,
onChange: @autoclosure () -> @Sendable () -> Void
) -> T {
let (result, accessList) = generateAccessList(apply)
if let accessList {
ObservationTracking._installTracking(accessList, onChange: onChange())
}
return result
}
アクセス履歴を保持するまでには以下の2つのステップを踏みます。
- _AccessListに一時的に保持
- ObservationRegistrarで履歴を集約する
_AccessListに一時的に保持
AccessListは、「どのオブジェクトのどのプロパティにアクセスしたか」を整理して記録している構造体です。
public struct _AccessList: Sendable {
// ObjectIdentifierはクラスのインスタンスを識別するためのID
internal var entries = [ObjectIdentifier : Entry]()
internal init() { }
internal mutating func addAccess<Subject: Observable>(
keyPath: PartialKeyPath<Subject>,
context: ObservationRegistrar.Context
) {
entries[context.id, default: Entry(context)].insert(keyPath)
}
// ..省略
}
struct Entry: @unchecked Sendable {
let context: ObservationRegistrar.Context
var properties: Set<AnyKeyPath>
// ..省略
}
たとえば、withObservationTrackingで以下のようにプロパティを参照したとします。
withObservationTracking {
print(chat.message)
print(chat.isRead)
} onChange: {
}
withObservationTracking内では、まずは generateAccessList(apply)
が実行されます。
// 省略版
func generateAccessList<T>(_ apply: () -> T) -> (T, _AccessList?) {
var accessList: _AccessList?
let result = withUnsafeMutablePointer(to: &accessList) { ptr in
// 現在のスレッドに追跡用AccessListポインタを設定
_ThreadLocal.value = UnsafeMutableRawPointer(ptr)
return apply() // この中でのプロパティアクセスが追跡される
}
return (result, accessList)
}
ここの apply()
実行時に、前述したObservationRegistrar.accessが呼ばれます。
public func access<Subject: Observable, Member>(
_ subject: Subject,
keyPath: KeyPath<Subject, Member>
) {
if let trackingPtr = _ThreadLocal.value?
.assumingMemoryBound(to: ObservationTracking._AccessList?.self) {
if trackingPtr.pointee == nil {
// 初回アクセス時に_AccessListインスタンスを作成
// このリストに、現在の追跡セッション中のアクセスを追加する
trackingPtr.pointee = ObservationTracking._AccessList()
}
trackingPtr.pointee?.addAccess(keyPath: keyPath, context: context)
}
}
ここでAccessListのインスタンスを生成し、アクセス履歴を追加してます。
最終的にはこのような形になります。
_AccessList {
entries = [
ChatのインスタンスのID: Entry(
context: Chatのインスタンス自身,
properties: [\.message, \.isRead]
)
]
}
ObservationRegistrarで履歴を集約する
次に_AccessListを元にして、ObservationRegistrarで履歴を集約します。
generateAccessList
が終わると、 ObservationTracking._installTracking(accessList, onChange: onChange())
が実行されます。
public static func _installTracking(
_ tracking: ObservationTracking,
willSet: (@Sendable (ObservationTracking) -> Void)? = nil,
didSet: (@Sendable (ObservationTracking) -> Void)? = nil
) {
let values = tracking.list.entries.mapValues {
switch (willSet, didSet) {
// 省略...
case (.some(let willSetObserver), .none):
return Id.willSet($0.addWillSetObserver { keyPath in
tracking.state.withCriticalRegion { $0.changed = keyPath }
willSetObserver(tracking)
})
// 省略...
}
}
tracking.install(values)
}
この $0.addWillSetObserver
のなかでObservationRegistrarに、監視対象のプロパティを渡しています。
struct Entry: @unchecked Sendable {
func addWillSetObserver(_ changed: @Sendable @escaping (AnyKeyPath) -> Void) -> Int {
return context.registerTracking(for: properties, willSet: changed)
}
}
internal mutating func registerTracking(for properties: Set<AnyKeyPath>, willSet observer: @Sendable @escaping (AnyKeyPath) -> Void) -> Int {
let id = generateId()
observations[id] = Observation(kind: .willSetTracking(observer), properties: properties)
for keyPath in properties {
lookups[keyPath, default: []].insert(id)
}
return id
}
この lookups
と observations
でプロパティへのアクセス履歴を保持しています。
- lookups:監視対象のプロパティと、監視IDを保持
- observations:監視IDと、監視対象プロパティが更新された時に実行するメソッドを保持
最終的に、それぞれに以下のような値が入ります。
lookups = [
\.isRead: [observerId1] // このプロパティを監視している観察者ID
\.message: [observerId1]
]
// 本来はObservationRegistrar.State.Observation型だが、簡略化している
observations = [
observerId1: { onChange() },
observerId2: { onChange() },
]
アクセス履歴を保持するまでのフローをシーケンス図に簡単にまとめます。

プロパティの変更を通知
ではここから、プロパティの変更を検知し、それを通知するまでの流れについて解説します。
こちらはアクセス履歴の保持よりは複雑ではないので、さらっと解説します。
まずプロパティが変更されると、 ObservationRegistrar.withMutation
が呼ばれます。
public func withMutation<Subject: Observable, Member, T>(
of subject: Subject,
keyPath: KeyPath<Subject, Member>,
_ mutation: () throws -> T
) rethrows -> T {
willSet(subject, keyPath: keyPath)
defer { didSet(subject, keyPath: keyPath) }
return try mutation()
}
}
mutation()で値が変更される前に、willSetが呼ばれます。
public func willSet<Subject: Observable, Member>(
_ subject: Subject,
keyPath: KeyPath<Subject, Member>
) {
context.willSet(subject, keyPath: keyPath)
}
internal struct Context: Sendable {
// .. 省略
internal func willSet<Subject: Observable, Member>(
_ subject: Subject,
keyPath: KeyPath<Subject, Member>
) {
let tracking = state.withCriticalRegion { $0.willSet(keyPath: keyPath) } // State.willSetを実行
for action in tracking {
action(keyPath) // onChangeで指定した処理を実行
}
}
// .. 省略
}
$0.willSet(keyPath: keyPath)
で、State.willSetを呼び出しています。
private struct State: @unchecked Sendable {
internal mutating func willSet(keyPath: AnyKeyPath) -> [@Sendable (AnyKeyPath) -> Void] {
var trackers = [@Sendable (AnyKeyPath) -> Void]()
if let ids = lookups[keyPath] { // 指定したkeypathがlookupsに登録されているかどうか(プロパティが監視対象かどうか)
for id in ids {
if let tracker = observations[id]?.willSetTracker {
trackers.append(tracker) // onChangeの処理が入る
}
}
}
return trackers
}
}
if let ids = lookups[keyPath]
で、指定したプロパティが監視されているかどうかを判定しています。この処理のおかげで、監視対象のプロパティが変更された時のみ、onChangeが実行されるようになっています。
ここまでのフローをシーケンス図に簡単にまとめます。

まとめ
今回はObservationの使い方と、その仕組みについて解説しました。
今後の開発では、Combineから移行されてどんどん主流のやり方となっていくと思います。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
参考にした記事


コメント