【Swift/Observation】@Observableマクロの使い方!状態監視の仕組み
この記事からわかること
- Swiftの@Observableとは?
- Observationフレームワークとは?
- 状態管理の実装方法
- @BindableやwithObservationTrackingの使い方
index
[open]
\ アプリをリリースしました /
環境
- Xcode:26.0.1
- iOS:26
- Swift:6
- macOS:Tahoe 26.0.1
Observationフレームワークとは?
ObservationフレームワークはSwift5.9 / iOS17から導入されたデータの変更を監視(observe)して変化を通知する新しい仕組みです。これまでデータの監視にはCombineベースの「ObservableObject + @Published」が活用されていましたが、これに置き換わる方法としてAppleも公式に推奨しています。
公式リファレンス:Migrating from the Observable Object protocol to the Observable macro
ObservationフレームワークではCombineから脱却しSwiftの言語機能(マクロ)で自動的に監視コードを生成することができるようになります。そしてObservationフレームワークで肝となるマクロが@Observableになります。Swift UIベースの開発で使いやすいように定義されていますがUIKitベースでも問題なく動作させることが可能です。
@Observableの使い方とObservableObjectとの違い
@Observableマクロの使い方を見る上で従来のObservableObjectプロトコルとの実装の違いも見ていきたいと思います。簡単なカウンターアプリを想定しています。
ObservableObject + @Published
まずは従来の方法だと以下のような実装になります。ViewModel側にはObservableObjectの準拠と値の変更監視対象を明示的に@Publishedを使用して宣言します。View側では@StateObjectなどを使用して変更を監視し、変化時に再描画が走るようにしておきます。
class CounterViewModel: ObservableObject { // ObservableObjectを継承
// 値の変更を監視対象にする
@Published var count = 0
func increment() { count += 1 }
}
struct ContentView: View {
// ObservableObjectを監視する
@StateObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button {
viewModel.increment()
} label: {
Text("increment")
}
}
}
}
@Observableマクロ
@Observableマクロの使い方は対象のViewModelに@Observableを付与するだけです。これにより対象のクラスのもつプロパティが全て自動で監視対象になります。View側では今回変更可能にしているので@Stateを付与していますが、@Stateの付与がなくても変更が走れば再描画されるようになっています。
@Observable
class CounterViewModel {
var count = 0
func increment() { count += 1 }
}
struct ContentView: View {
// View内で変更しているので@Stateを付与
@State private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button {
viewModel.increment()
} label: {
Text("increment")
}
}
}
}
@Observableの特徴
公式リファレンス:Managing model data in your app
特徴まとめ
- 構造体(値型のオブジェクト)には使えない
- computed property(計算プロパティ)も監視対象
- Viewから参照している部分が変化する場合に再描画が走る
構造体には使えない
@Observableは構造体(struct)では使用できずクラス(class)のみで使用できます。これはObservationフレームワークが参照型のオブジェクトを対象としているからです。値型の構造体では都度インスタンスのコピーが作られる仕組みのため変更の監視を行うことができないようです。
@Observable // ✖︎
struct Book: Identifiable {
var title = "Sample Book Title"
var author = Author()
var isAvailable = true
}
computed property(計算プロパティ)も監視対象
@Observableを付与したクラスではcomputed property(計算プロパティ)も監視対象になります。例えばavailableBooksCount(コレクションの特定の条件にマッチするカウント)を計算するプロパティを用意しておいた場合に、任意のコレクションの要素のisAvailableに変化が起きた場合にViewに対して変化を通知することが可能です。
@Observable class Library {
var books: [Book] = [Book(), Book(), Book()]
var availableBooksCount: Int {
books.filter(\.isAvailable).count
}
}
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
NavigationStack {
List(library.books) { book in
// ...
}
.navigationTitle("Books available: \(library.availableBooksCount)")
}
}
}
再描画の依存関係
Apple公式では@Observableを使ったサンプルコードとしてモデル自体を監視可能にしています。例えば以下のように複数の監視対象となるプロパティを持つモデルクラスがあるとします。
@Observable
final class Book: Identifiable {
var title = "Sample Book Title"
var author = Author()
var isAvailable = true
}
これをView内で使用する際には以下のようになりますが、この際にView内で読み取っているプロパティの変化のみが再描画のトリガーになります。つまり今回で言うとauthorやisAvailableに変化が起きてもBookView自体は再描画されません。
struct BookView: View {
var book: Book
var body: some View {
Text(book.title)
}
}
またコレクション型で保持している場合はコレクションに対する要素の追加・削除も変更監視対象となり期待通りに再描画されるようになります。またここで嬉しいのは1つの要素(Book)のプロパティが更新された際にList全体ではなく、対象のBookを参照しているViewのみが再描画されるように最適化されています。
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
List(books) { book in
Text(book.title)
}
}
}
@Bindable
@Bindableを使用すると双方向バインディングを実現することが可能です。View側での更新をリアルタイムでBookモデル側にも反映させることができるようになります。
struct BookEditView: View {
@Bindable var book: Book
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
TextField("Title", text: $book.title)
Toggle("Book is available", isOn: $book.isAvailable)
Button("Close") {
dismiss()
}
}
}
}
withObservationTracking
ViewではなくView以外の部分やUIKit側からObservationフレームワークを活用するにはwithObservationTrackingを使用します。withObservationTrackingは低レベル層のAPIで@Observableなどで内部的に自動でやってくれていた部分を手動で実装できる役割を持っています。
public func withObservationTracking<T>(_ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void) -> T
applyでは監視対象を参照するように実装し、onChangeで変化が起きた際に実行したい処理を実装します。言葉では理解しづらいのでコードで確認してみます。
@Observable
class Counter {
var count = 0
}
let counter = Counter()
// 観測をセットアップ
withObservationTracking {
// counter.countを監視対象にしたいため参照を持たせる
print("現在のカウント:\(counter.count)")
} onChange: {
// 変更が起きた際に実行したい処理
print("カウントが変更: \(counter.count)")
}
上記のように1つ目のクロージャーの中ではただ値を参照するだけ、2つ目のクロージャーの中で変化した際に発火させたい処理を記述します。またwithObservationTrackingでの監視は1回だけなので2回目の変更があっても期待通りに動作しません。
そのため永続的に監視を続けたい場合はonChangeの中で再起的にwithObservationTrackingを呼び出すような構造で実装してあげる必要があります。
private func tracking() {
withObservationTracking {
// 〜〜〜〜
} onChange: {
tracking()
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





