質問の歴史
Swift 5.9以前、SwiftUIにおけるリアクティブプログラミングはObservableObjectプロトコルとCombineフレームワークに依存していました。開発者はプロパティに**@Published**を手動で付与し、パブリッシャーを合成するか、objectWillChange.send()を呼び出してビューの変更を通知しました。このパターンは粗い粒度の更新に悩まされていました—プロパティの変更が発生すると、全体のビュー本体が再評価され、構造体を複雑なビュー・モデルに使用することを妨げる参照セマンティクスが義務付けられていました。Observationフレームワークは、明示的なパブリッシャー宣言なしで高度な自動反応性を提供するために導入されました。
問題点
コアの課題は、明示的なボイラープレートなしでプロパティのアクセスと変更を検出し、型安全性と高性能を維持することでした。従来のソリューションは、手動のキー・バリュー・オブザーブ(KVO)を必要とし、これは文字列型で脆弱であったり、ドメインモデルを混乱させるラッパー型が必要でした。システムは、実行時のオーバーヘッドやObservableObjectの参照カウントされたアイデンティティの伝播のアーキテクチャ的制約なしに、依存関係を動的に登録するために、読み取りと書き込みの操作を intercept する必要がありました。
解決策
Swiftは、ピア、メンバー、およびアクセサーマクロ機能を組み合わせた**@Observableマクロを使用しています。コンパイル時に、このマクロは注釈を付けたクラスをプライベートなObservationRegistrarインスタンスを注入することで変換します。それから、すべてのストレージプロパティを書き換えて、読み取りを_$observationRegistrar.track(self, keyPath: ...)でラップし、書き込みをwillSet/didSet通知でラップする計算アクセサに変換します。この展開は、自動的にObservableプロトコルへの適合を合成し、必要なobservationRegistrar計算プロパティを実装します。SwiftUIは、ビュー本体評価中にこのレジストラに統合し、実際にアクセスされるプロパティのみに対して自身をオブザーバーとして登録することで、手動のCombine**セットアップなしで粒度の高い更新を実現しました。
@Observable class SettingsViewModel { var isDarkModeEnabled = false var notificationCount = 0 } // 概念的なコンパイラの展開: class SettingsViewModel { private let _$observationRegistrar = ObservationRegistrar() var isDarkModeEnabled: Bool { get { _$observationRegistrar.track(self, keyPath: \.isDarkModeEnabled) return _isDarkModeEnabled } set { _$observationRegistrar.willSet(self, keyPath: \.isDarkModeEnabled) let oldValue = _isDarkModeEnabled _isDarkModeEnabled = newValue _$observationRegistrar.didSet(self, keyPath: \.isDarkModeEnabled, oldValue: oldValue) } } private var _isDarkModeEnabled = false // ... notificationCountのための同様のパターン }
あなたは、ライブ株価、ユーザーのポートフォリオ合計、ニュースフィードを表示するリアルタイム金融ダッシュボードSwiftUIアプリケーションのアーキテクチャを設計しています。ビュー・モデルには、ブール型のUIフラグから複雑なチャートデータ構造まで、30の異なるプロパティが含まれています。
当初、チームはすべてのプロパティに**@Publishedラッパーを付けてObservableObjectを使用して実装しました。これにより、大幅なパフォーマンス低下が発生しました:1つの株価が更新されると、全体のダッシュボードが再計算されました。なぜなら、ObservableObjectは「何かが変わった」とオブジェクト全体に通知し、粒度が欠如していたからです。また、コードは冗長で、リピートする@Published var宣言や、サブスクリプションのメモリリークを防ぐための手動のAnyCancellable**ストレージを必要としました。
チームは、パフォーマンスとボイラープレートの問題を解決するための3つのアーキテクチャアプローチを評価しました。
最初のアプローチは手動のCombine最適化でした。彼らは、重要なプロパティごとに個別のPassthroughSubjectインスタンスを作成し、特定の更新を指定して.unsubscribeするために.onReceiveを使用しました。利点は、どのUIコンポーネントが更新されるかについての正確な制御でした。しかし、欠点は、30のサブジェクトが30のサブスクリプションを必要とし、**Set<AnyCancellable>**で手動のメモリ管理が難しく、大規模なコード膨張を引き起こしたことでした。このため、コードベースは保守不可能になりました。
2番目のアプローチは、SwiftUIの@Stateを使用して値型のビュー・モデルにすることを提案しました。彼らは、ビュー・モデルを不変の値として扱い、すべての変更時にそれを置き換えることにしました。利点は、自然な値セマンティクスと、自動的な等価チェックが冗長な更新を防ぐことでした。しかし、欠点は、参照アイデンティティの喪失でした;すべての変更が新しいインスタンスを作成し、ScrollViewの位置復元を壊し、アイデンティティの喪失のために複雑なオブジェクトの調整が不可能になりました。
3番目のアプローチは、@Observableマクロを採用しました。@Observableでクラスに注釈を付け、すべての**@Published属性を削除することにより、コンパイラが自動的にプロパティをObservationRegistrar**を使用するように変換しました。利点は二つありました:構文は通常のvar宣言のままで、SwiftUIは、各ビューの本体でアクセスされたプロパティを自動的に追跡して、特定のサブビューのみを更新しました。欠点は、Swift 5.9に移行し、チームにマクロのデバッグ技術を教育する必要があったことです。
チームは、Combineのサブスクリプションコードを200行削除しながら粒度の問題を解決するこの3番目のアプローチを選択しました。彼らは、高頻度の価格更新中にCPU使用率が40%減少したことを観察しました。その結果、個々の株価ラベルが独立して更新され、静的なニュースフィードセクションのレイアウト再計算を引き起こすことなく、応答性の高いダッシュボードが実現しました。
なぜObservationフレームワークは、観察される型がクラス(参照セマンティクス)である必要があり、@Observableが構造体に適用されるときにどのようなコンパイルエラーが発生するのか?
@Observableマクロは、ストレージプロパティとしてObservationRegistrarを注入し、Observableプロトコルを実装することによって展開されます。構造体は値型であり、コピーオンワ写セマンティクスがあります;各変更は概念的に新しいインスタンスを作成します。そのため、ObservationRegistrarは内部の状態(オブザーバーリストやトラッキングスコープ)を維持する必要があり、この状態は変更を跨いで持続しなければなりません。構造体に適用すると、変更がレジストラ状態を正しくコピーし、オブザーバーとインスタンスの接続を壊してしまいます。コンパイラは、マクロが必要なストレージプロパティを値型に追加できないと示すエラーを生成することによって、これを防ぎます。具体的には、結果の型が安定したアイデンティティの要件を満たさないため、Observableに適合できないということです。
Observationフレームワークは、ネストされたオブザーバブルオブジェクトをどのように扱い、なぜ**@Publishedのような個々のプロパティに対する投影値(例えば、$property)がないのか?
@Observableクラスに自身が**@Observableクラスのプロパティを含む場合、フレームワークはプロパティレベルでのアクセスを追跡し、ネストされたオブジェクトを自動的に再帰的に観察することはありません。outer.inner.nameにアクセスすることは、outerオブジェクトのinnerプロパティに対する依存関係を登録します。innerインスタンスが完全に置換されると、オブザーバーに通知されます。しかし、inner.nameの変更は、outerオブジェクトのオブザーバーには通知されません。Combineとは異なり、Observationにおける個々のプロパティに対する投影値概念はありません。なぜなら、フレームワークはパブリッシャーストリームではなく、レジストラを介して直接プロパティを追跡するからです。SwiftUIにおけるObservationの$構文は、@Bindable**でラップされたインスタンス全体に対して使用され(例:@Bindable var viewModel: SettingsViewModelは$viewModel.isDarkModeEnabledを許可します)、個々のプロパティ宣言に対しては使用されません。
Observationフレームワークは、どのような具体的なスレッド安全保障を提供し、Swiftの同時 Actor とどのように相互作用するのか?**
ObservationRegistrar自体は本質的にスレッド安全ではなく、観察可能オブジェクトへのシリアライズされたアクセスを前提としています。@ObservableクラスがActorに隔離されると(例えば@MainActor)、すべての変更と観察が自動的にそのActorのコンテキストで発生し、データ競合を防ぎます。フレームワークは、オブザーバーの隔離ドメインを尊重するために、オブザーバーのコールバックがSendableチェックを使用することを保証します。重要な実装の詳細は、トラッキングメカニズムが、ビュー本体の実行中に現在の観察スコープを維持するためにTaskLocalストレージを使用することです。つまり、観察登録は、登録された特定の非同期トランザクション中にのみアクティブであり、明示的な転送なしに無構造の同時境界を超えて漏れることはありません。