最終更新:2026年7月19日
@ObservableマクロはSwiftUIの新しい観測システム(Observation framework)で、iOS 17以降で利用可能なObservableObjectプロトコルの後継です。@Observableは@Publishedを不要にし、ビューが実際に読み取ったプロパティだけを追跡することで不要な再描画を防ぎ、Combineに依存しないSwiftネイティブなAPIを提供します。ただし完全なドロップイン置換ではなく、@StateObjectから@Stateへの切り替えなど初期化挙動に注意が必要です。
正直なところ、私自身も昨年の実案件でObservableObjectから@Observableへ移行した際、@StateObjectを@Stateに機械的に置換して初期化が毎回走ってしまうバグを踏みました(結局はイニシャライザの副作用が原因だったのですが)。この記事では両者の違い、実装コード、移行手順、そしてよくある落とし穴を、2026年時点の最新情報と、実際に本番投入して見えたポイントを交えて解説します。
@Observableマクロとは何か?
@Observableは、Swift 5.9で導入されたObservation frameworkの中心にあるマクロで、参照型(class)を宣言的にSwiftUIの観測対象にできます。iOS 17、macOS 14、watchOS 10、tvOS 17以降で利用可能です。開発現場で長年使われてきたCombineベースのObservableObjectプロトコルと@Publishedプロパティラッパーの組み合わせを置き換え、より少ないボイラープレートで、より細粒度な変更追跡を実現します。
従来のObservableObjectでは、あるプロパティを観測対象にするには@Publishedを付ける必要がありました。付け忘れるとビューが更新されない静かなバグにつながり、SwiftUI初心者が最も詰まりやすいポイントの一つでした(私も駆け出しの頃、これで半日溶かしたことがあります)。@Observableではこの明示的な指定が逆転し、すべての格納プロパティが自動的に観測対象になります。観測から除外したい場合だけ@ObservationIgnoredを付けるという「オプトアウト」のモデルです。
import Observation
@Observable
final class CounterModel {
var count: Int = 0
var lastUpdated: Date = .now
// 観測から除外したいプロパティは明示的にオプトアウトする
@ObservationIgnored
private var undoStack: [Int] = []
func increment() {
undoStack.append(count)
count += 1
lastUpdated = .now
}
}マクロが展開するコードでは、内部的にObservation.Observableプロトコルへの準拠、_$observationRegistrarの生成、各プロパティのgetter/setterへのwithObservationTracking呼び出しの挿入が行われます。開発者が書くのは@Observableとクラス本体だけで、残りはコンパイル時にマクロが生成します。
@ObservableとObservableObjectの主な違い
両者は「参照型を観測する仕組み」という点では同じですが、依存するフレームワーク、プロパティの扱い、ビューでの受け取り方、パフォーマンス特性のすべてで異なります。以下の比較表は、実際にコードを書き分けるときに参照すべき主要な相違点を1画面にまとめたものです。移行中の判断に迷ったら、まずこの表を見返しています。
| 項目 | ObservableObject | @Observable マクロ |
|---|---|---|
| 依存フレームワーク | Combine | Observation(Swiftネイティブ) |
| プロパティのマーク | @Published が必須 | 自動追跡(除外は@ObservationIgnored) |
| 所有ビューの宣言 | @StateObject | @State |
| 子ビューへの受け渡し | @ObservedObject | プレーンなプロパティ or @Bindable |
| Environment 経由 | @EnvironmentObject | @Environment |
| 再描画の粒度 | @Publishedの発火=観測ビュー全再描画 | 実際に読んだプロパティのみが対象 |
| 最小デプロイターゲット | iOS 14+ | iOS 17+ / macOS 14+ |
| Swift Concurrency との親和性 | @Published の型が限定的 | プロパティ型を選ばず自然に統合 |
特に重要なのは再描画の粒度です。ObservableObjectでは、@Publishedプロパティのどれか一つが変更されると、その@ObservedObject/@StateObjectを保持しているビューは、実際にはそのプロパティを読んでいなくても再評価されます。@Observableでは、SwiftUIが"body評価中にどのプロパティを読んだか"を追跡し、そのプロパティが変わったときだけbodyを再実行します。大きなモデルを持つ画面ほど、この差は体感できる速度差になります。
もう一つの見落とされがちな違いは、Swift 6.2の並行処理モデルとの統合です。@Publishedはpublisher型を露出するためSendable境界の扱いが複雑になりがちですが、@Observableは通常の格納プロパティとして振る舞うため、@MainActor隔離下でも素直に書けます。
ObservableObjectから@Observableへの移行手順
さて、実装の話に入ります。既存のObservableObjectベースのビューモデルを@Observableに移すには、モデル側とビュー側の両方に手を入れる必要があります。以下は、典型的なチェックリストと、変換前後のコードです。基本的な流れは、(1)モデルからCombineの痕跡を消し、(2)ビュー側のプロパティラッパーを新しいAPIに置き換える、という順序です。私の経験上、モデル側を先に済ませてビルドを通してから、ビュー側を1画面ずつ移すのが一番事故が少なかったです。
ステップ1:モデルクラスの変換
// Before(ObservableObject)
import Combine
final class UserProfileViewModel: ObservableObject {
@Published var name: String = ""
@Published var email: String = ""
@Published var isLoading: Bool = false
private var cancellables = Set<AnyCancellable>()
func load(userId: String) async {
isLoading = true
defer { isLoading = false }
// ...ネットワーク処理
}
}
// After(@Observable)
import Observation
@Observable
final class UserProfileViewModel {
var name: String = ""
var email: String = ""
var isLoading: Bool = false
@ObservationIgnored
private var loadTask: Task<Void, Never>?
func load(userId: String) async {
isLoading = true
defer { isLoading = false }
// ...ネットワーク処理
}
}ステップ2:ビュー側プロパティラッパーの置換
// Before
struct ProfileView: View {
@StateObject private var viewModel = UserProfileViewModel()
var body: some View {
Form {
TextField("名前", text: $viewModel.name)
if viewModel.isLoading { ProgressView() }
}
}
}
// After
struct ProfileView: View {
@State private var viewModel = UserProfileViewModel()
var body: some View {
Form {
// @Bindableで双方向バインディングを取り出す
@Bindable var vm = viewModel
TextField("名前", text: $vm.name)
if viewModel.isLoading { ProgressView() }
}
}
}ステップ3:子ビューへの受け渡しを見直す
子ビューでは@ObservedObjectを削除し、通常の格納プロパティとして受け取ります。双方向バインディングが必要な場合だけ@Bindableを使います。
// Before
struct EmailField: View {
@ObservedObject var viewModel: UserProfileViewModel
var body: some View {
TextField("Email", text: $viewModel.email)
}
}
// After
struct EmailField: View {
@Bindable var viewModel: UserProfileViewModel
var body: some View {
TextField("Email", text: $viewModel.email)
}
}@Observableのパフォーマンス改善の仕組み
@ObservableがObservableObjectよりも高速になる理由は、プロパティ単位のトラッキングにあります。従来のObservableObjectでは、SwiftUIは"objectWillChange"という単一のパブリッシャーを購読していました。どのプロパティが変更されようと同じシグナルが飛び、そのモデルを@ObservedObject/@StateObjectで持つビューは全員bodyを再評価する必要がありました。
@Observableでは、SwiftUIはwithObservationTrackingブロックで囲まれたbody評価中に、モデルのどのプロパティが読まれたかを記録します。次に更新すべきかを判定する際、そのビューが読んだプロパティが実際に変わった場合だけbodyを再実行します。これは仮想DOMでいう"selector購読"に近い最適化で、ダッシュボード的なビューほど恩恵が大きくなります。
@Observable
final class Dashboard {
var visitors: Int = 0
var revenue: Double = 0
var alerts: [String] = []
}
struct VisitorTile: View {
let dashboard: Dashboard
var body: some View {
// このビューは visitors だけ読む
Text("訪問者: \(dashboard.visitors)")
}
}
// alerts が更新されても VisitorTile.body は再実行されない
// ObservableObject 版なら、alerts の @Published 変更で
// VisitorTile も再評価されていたApple自身の測定でも、@Observableは複雑なビューヒエラルキーで数百マイクロ秒〜数ミリ秒レベルのbody評価時間短縮を報告しています。とくにiOS 26のLiquid Glassデザインのように、多くの半透明マテリアルと動的レイアウトが重なるUIでは、再描画の削減がスクロール中のフレームレートに直接効いてきます。うちの案件でも、リスト画面のスクロール時のフレームドロップが目に見えて減りました。
@Bindableプロパティラッパーの使い方
@Bindableは、@Observableで観測されるクラスのプロパティに対して、Binding<T>を取り出すためのプロパティラッパーです。SwiftUIのTextField、Toggle、Slider、Pickerなど、双方向バインディングを要求するコントロールで必要になります。ObservableObject時代の@ObservedObjectと同じ役割ですが、目的が「バインディングを取り出す」ことだけに絞られている点が異なります。
@Bindableは3つの場所で使えます。ビューのプロパティとして、bodyのローカル変数として、そしてEnvironmentから取り出した値を再ラップするときです。
// 1. ビュープロパティとして(子ビューが親から受け取る)
struct SettingsView: View {
@Bindable var settings: SettingsModel
var body: some View {
Toggle("通知", isOn: $settings.notificationsEnabled)
}
}
// 2. bodyのローカル変数として(@Stateで持つ場合)
struct EditorView: View {
@State private var document = Document()
var body: some View {
@Bindable var doc = document
TextField("タイトル", text: $doc.title)
}
}
// 3. Environment経由でもBindingが必要なとき
struct ThemeToggle: View {
@Environment(ThemeStore.self) private var store
var body: some View {
@Bindable var store = store
Toggle("ダークモード", isOn: $store.isDark)
}
}注意すべきは、@Bindableは値の所有権を移さないという点です。あくまで既存の@Observableインスタンスからバインディングを取り出すためのシンタックスシュガーであり、モデルの寿命は元の場所(親ビューの@State、Environmentなど)が管理します。
@Environmentでの@Observable共有
アプリ全体でモデルを共有したい場合、ObservableObject時代は@EnvironmentObjectと.environmentObject(_:)のペアを使っていました。@Observableでは、これが@Environmentと.environment(_:)に統合されます。既存のEnvironment values(\.colorSchemeなど)と同じ経路に乗るため、キーが型そのものになるという意外な変更があります。
@Observable
final class AuthSession {
var user: User?
var isSignedIn: Bool { user != nil }
}
@main
struct MyApp: App {
@State private var auth = AuthSession()
var body: some Scene {
WindowGroup {
RootView()
.environment(auth) // 型そのものをキーに使う
}
}
}
struct ProfileButton: View {
@Environment(AuthSession.self) private var auth
var body: some View {
if auth.isSignedIn {
Label(auth.user?.name ?? "", systemImage: "person")
} else {
Button("サインイン") { /* ... */ }
}
}
}@EnvironmentObjectとの重要な違いは、実行時のクラッシュ挙動です。@EnvironmentObjectは.environmentObjectが祖先にない状態でアクセスするとfatalErrorで落ちます。@Environment(_:)はデフォルトでは同様にクラッシュしますが、iOS 17以降のSwiftUIランタイムは診断ログをより詳細に出すようになっており、原因特定が早くなっています。
@Observable使用時によくある落とし穴
@Observableは書きやすくなった一方で、ObservableObjectとは違う細かい落とし穴があります。以下は、実際にプロダクションコードで遭遇しやすいトップ5(体感、というやつです)です。1と3は特にレビュー時に指摘することが多い項目です。
1. 構造体プロパティの変更が伝わらないケース
@Observableは格納プロパティへの代入を追跡します。プロパティが構造体(配列や辞書を含む)の場合、その中身を書き換えても、SwiftUIから見れば「同じ値のまま」に見えることがあります。基本型(Int、String、Bool)や、配列全体を差し替える操作なら問題ありません。
2. Computed propertyは自動で追跡されない
計算プロパティ(var isValid: Bool { name.isEmpty == false }など)は、依存する格納プロパティが観測対象であれば結果的に更新されますが、Observation frameworkが直接それを追跡しているわけではありません。パフォーマンスセンシティブな場面では、計算結果をキャッシュした格納プロパティに置き換えることを検討します。
3. 初期化タイミングの誤解
@StateObjectと違い、@Stateで宣言したイニシャライザ引数は毎回評価されるという点に注意が必要です。@State var vm = ExpensiveModel(dependency: heavyLoad())のように書くと、実際にはSwiftUIが初回だけ結果を採用するとはいえ、heavyLoad()の呼び出し自体はビュー再構築のたびに走ります。重い依存はコンテナ側で作ってから渡す設計にします。
4. iOS 16以下との互換性
iOS 16以下をサポートするアプリでは、@Observableはif #availableで分岐してもコンパイル自体できません。マクロ展開時に必要なランタイムシンボルが古いOSにないためです。マルチOS対応が必要な場合は、条件付きコンパイルでモデル定義自体を分ける必要があります。
5. @ObservedObjectを@Observableと混同する
Combineに@ObservedObject、Observationに@Observableと、名前が似ているため入れ替えて書いてしまうミスがよく発生します。マクロは@Observable(クラスに付ける)、旧APIのプロパティラッパーは@ObservedObject(ビューに付ける)と役割が根本的に違うことを覚えておきます。
よくある質問
@ObservableとObservableObjectはどちらを使うべきですか?
iOS 17以降のみをサポートする新規プロジェクトでは、迷わず@Observableを選ぶべきです。パフォーマンス、コード量、Swift Concurrencyとの統合すべてで優れています。iOS 16以下のサポートが必要な既存アプリでは、当面ObservableObjectを維持し、デプロイターゲットが上がったタイミングで移行する戦略が現実的です。
@Observableは@Publishedの完全な置き換えですか?
役割としては置き換えですが、機能的には拡張されています。@Publishedは個別プロパティを観測対象にしていましたが、@Observableでは全プロパティが自動で観測対象になり、除外したい場合だけ@ObservationIgnoredを付けます。「オプトイン」から「オプトアウト」への発想の転換です。
@Bindableと@Bindingの違いは何ですか?
@Bindingは値型のBindingを子ビューへ渡すための古くからあるプロパティラッパーです。@Bindableは、@Observable参照型のプロパティからBindingを取り出すための新しいラッパーで、iOS 17+専用です。値型なら@Binding、@Observableクラスなら@Bindable、と使い分けます。
@Observableに移行するとCombineは使えなくなりますか?
いいえ、Combineは引き続き使えます。@Observableで観測を宣言しつつ、必要に応じてCombineのPublisherも並行して定義できます。ただし、SwiftUIとの連携目的だけでCombineを維持する理由はほぼなくなるため、多くのプロジェクトでCombine依存が徐々に減っていくのが自然な流れです。
@Observableクラスは@MainActorで隔離すべきですか?
UI状態を保持するビューモデルであれば@MainActorで隔離するのが安全で、Swift 6.2の"approachable concurrency"方針とも合致します。ネットワーク層や純粋なデータ処理層に近いモデルはメインアクター隔離を避け、必要な場面だけメインへホップするほうがCPUリソースを効率よく使えます。




