@Observable(新しい書き方)
✔ @EnvironmentObject は 将来的に非推奨方向
macOS 14 / iOS 17 の Observation フレームワークの登場により、
ObservableObject@Published@EnvironmentObject
は だんだん使わなくなる方向。
新しい書き方
① 共有設定クラス
|
1 2 3 4 5 6 |
import Observation @Observable class AAA { var a: String = "Hello" } |
② App 起動時に environment に注入する
|
1 2 3 4 5 6 7 8 9 10 11 |
@main struct MyApp: App { @State var BBB = AAA() var body: some Scene { WindowGroup { ContentView0() .environment(BBB) // ← iOS の .environmentObject と同じ役割 } } } |
③ 各 View からはこう呼ぶ
|
1 2 3 4 5 6 7 8 9 10 |
import SwiftUI import Observation struct ContentView0: View { @Environment(AAA.self) var BBB // ← 新しい書き方 var body: some View { Text(BBB.a) } } |
✔ 新パターンは「environment に任意の型を注入する」
🎁 iOS の書き方との比較表
| 目的 | iOS 16以前 | macOS 14/iOS 17の新方式 |
|---|---|---|
| 共有設定クラス | ObservableObject + @Published | @Observable |
| 全Viewで共有 | .environmentObject | .environment(AppSettings()) |
| 参照 | @EnvironmentObject var settings | @Environment(AppSettings.self) var settings |
| 自動更新 | Combineベース | メモリ効率の良い SwiftMacro ベース |
🔧 Tips:@StateObject や バインディング例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct SettingsView: View { @State var CCC = AAA() var body: some View { Text(CCC.a) VVV(DDD: CCC) } } struct VVV: View { @Bindable var DDD: AAA var body: some View { Text(DDD.a) } } |
@EnvironmentObject(古い)
全View間で使えるグローバル変数的なものです。
変数に変更が掛かるとすべてのViewで連動して変更されます。
注意点として、最上位のViewに.environmentObject()で渡してあげる事とEnvironmentObjectをインスタンス化したらすべてのプレビューの所でもインスタンス化してあげる事。
あとはグローバル変数の危険性を知っておくと良いかもしれません、似たようなものなので。
@EnvironmentObject の役割
@EnvironmentObject は、Viewとその直接的なViewロジックにおいて、データの受け渡しを簡略化するためにSwiftUIが提供する機能です。
- View階層の深くにあるViewでも、プロパティとして宣言するだけで簡単にデータにアクセスできるメリットがあります。
- 主にViewを再構築するためのデータソースとして使用されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import SwiftUI class aaa: ObservableObject { //用意するクラス @Published var a: Int = 0 } struct ContentView: View{ @EnvironmentObject var b: aaa // = ではなく : でインスタンス化 var body: some View { Button(action: { b.a += 1 }){ Text("\(String(b.a))") } } } struct ccc: View { //遷移先等 @EnvironmentObject var b: aaa var body: some View { Text("\(String(b.a))") } } //最上位のViewに .environmentObject(aaa() を渡す @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .environmentObject(aaa() } } } #Preview { ContentView() .environmentObject(aaa()) //EnvironmentObjectをインスタンス化したすべてのページでこの記述をする(クラス名は作ったクラス名) } |
@StateObject
こちらはインスタンス化したViewの中でのみ状態変化を保持します。(View毎でインスタンス化すると別物扱いになります)
なので他のViewで変数の状態変化を共有させたい時は引数で渡す必要があります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import SwiftUI class aaa: ObservableObject { //用意するクラス @Published var a: Int = 0 } struct ContentView: View{ @StateObject var b = aaa() //こちらは = で通常のインスタンス化 var body: some View { Button(action: { b.a += 1 }){ Text("\(String(b.a))") } ccc(c: b) //他Viewに渡す } } struct ccc: View { @StateObject var c: aaa //受け取り var body: some View { Text("\(String(c.a))") } } #Preview { ContentView(b: aaa()) } |
@StateObject と #Preview の関係
1. @StateObject の役割(生成と所有)
@StateObject は、ビューが初めて初期化されるときに、その場で新しいObservableObjectのインスタンスを生成します。
|
1 2 3 4 5 |
struct APIResultView: View { // 💡 Viewがこのインスタンスの所有者(オーナー)となる @StateObject var manager = APIManager() // ... } |
このため、#Preview でこのビューをインスタンス化するときも、APIResultView が内部で APIManager() を生成するため、外部から APIManager のインスタンスを注入(提供)する必要がありません。
2. #Preview の記述
@StateObject を使用する場合の #Preview の記述は、非常にシンプルになります。
|
1 2 3 4 5 |
// APIResultView 内の @StateObject を使用する場合 #Preview { // APIResultView() を直接インスタンス化するだけでOK APIResultView() } |
3. @EnvironmentObject の場合(対比)
一方で、@EnvironmentObject を使用する場合、ビューは外部からインスタンスが提供されることを期待します。
|
1 2 3 4 5 |
struct APIResultView: View { // ⚠️ 外部からの注入を期待 @EnvironmentObject var manager: APIManager // ... } |
このため、#Preview でもその期待に応える必要があり、エラーを避けるために .environmentObject() を使ってインスタンスを「注入」する必要がありました。
|
1 2 3 4 5 6 |
// @EnvironmentObject を使用する場合 #Preview { APIResultView() // 外部から提供(注入)が必須 .environmentObject(APIManager()) } |
注意点
SwiftUI管轄外、Viewを使わないクラス等では直接インスタンス化出来ない。
エラー指摘されずクラッシュします。
通常のクラスにObservableObjectを渡したい時は明示的に渡す。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import SwiftUI class aaa: ObservableObject { @Published var b: Int = 0 } class bbb { //通常のクラス var a: aaa init(aa: aaa) { self.a = aa } func c() { print(a.b) } } struct TestView: View { @EnvironmentObject var aa: aaa var body: some View { VStack { Text("Hello") } .onAppear { let ddd = bbb(aa: aa) ddd.c() } } } #Preview { TestView() .environmentObject(aaa()) } |
GPTさん曰く
「SwiftUIって、@EnvironmentObjectやObservableObjectの依存関係が裏で勝手に解決されるから、
「ただクラスをnewしただけで使える」と思いがちなんですよね。
でも@EnvironmentObjectが付いたクラスは基本「SwiftUI側がインスタンスを注入する前提」なので、
自前で init() しても nil のまま… → そこでクラッシュ、というのがよくあるパターンです💦」
Simulatorは大丈夫でもPreviewがクラッシュしやすい
非同期に処理結果をViewに反映している場合、その処理の順番が実機での順番とPreviewでの順番が違うためSwiftUI が環境をまだ用意していない状態でアクセスしてクラッシュすることがある。
自分は他のViewと共通して持っていたObservableObjectのデータを使ってAPI通信をし、結果をViewに表示するViewに渡していた為この問題が起きた。
PreviewはそのView単体表示させる為他のViewのObservableObjectデータ内容を知ることができない。
回避策はPreviewにダミーデータを渡す。
|
1 2 3 4 5 6 7 8 |
#Preview (body: { let sv = SharedVariables(). //ソースコード内とは別でインスタンス化する、インスタンス化の方法は違っていても問題ない sv.send_name = "稚内" //ネットワークで使う変数に仮のものを入れる sv.latitude = 45.414 sv.longitude = 141.685 return WeatherSheet() //return を付ける .environmentObject(sv) //仮でインスタンス化したオブジェクトを入れる }) |
補足:なぜプレビューだけ落ちるのか
・プレビューはアプリ全体じゃなく「View単体」を再現するだけなので・@EnvironmentObject の「親View」が存在しない
・SwiftUIプレビューの裏では App の @main や Scene が動いていない
・非同期処理 (async) や .task の呼び出しが開始前に nilアクセスを起こす
・Simulatorでは App 全体が立ち上がるため正常
ObservableObject のデータクリア方法(View変える時とか)
1. データのクリアをViewのライフサイクルに結合する (推奨)
最も一般的で、SwiftUIのライフサイクルに沿った手法です。データを保持しているObservableObjectとは独立して、Viewが消えるタイミングでクリア関数を呼び出します。
手法:onDisappear の利用
API結果を表示するViewに .onDisappear モディファイアを追加し、その中でObservableObjectに用意したクリア用メソッドを呼び出します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class APIManager: ObservableObject { @Published var apiResults: [String] = [] // データをクリアするための専用メソッド func clearResults() { apiResults = [] print("🧹 API結果をクリアしました。") } } struct APIResultView: View { @ObservedObject var manager: APIManager // または @EnvironmentObject var body: some View { // ... API結果の表示 ... .onDisappear { // Viewが画面から消えるとき(閉じる、戻るなど)に実行 manager.clearResults() } } } |
メリット:
- 明確な意図: いつデータをクリアするかがコードで明確になります。
- シンプル:
ObservableObject側にロジックを追加するだけで済みます。
2. 新しいインスタンスを生成する (StateObjectの利用)
表示Viewに遷移するたびにデータがクリアされた状態から始まるのが望ましい場合、@StateObjectを使用してViewのライフサイクルとObservableObjectのインスタンスのライフサイクルを結合します。
手法:@StateObject を使用
@StateObjectで宣言されたインスタンスは、そのViewが生存している間だけメモリに保持されます。Viewが破棄されると、@StateObjectのインスタンスも一緒に破棄され、その中のデータは解放されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 親Viewの例 struct ParentView: View { // NavLinkやButtonでAPIResultViewに遷移 var body: some View { NavigationStack { NavigationLink("API Viewへ遷移") { // 遷移先のView内で @StateObject としてインスタンス化 APIResultView() } } } } struct APIResultView: View { // Viewが表示されるたびに新しいインスタンスが生成される @StateObject var manager = APIManager() var body: some View { // ... API結果の表示 ... .onAppear { // 新しいインスタンスなので、ここでAPI呼び出し manager.fetchData() } } } |
メリット:
- 自動クリーンアップ:
clearResults()のような手動の呼び出しが不要で、Viewが閉じるとインスタンスごと解放されます。 - クリーンな状態: 遷移するたびに、常に初期状態からデータがロードされます。
デメリット:
- データが別のViewやアプリ全体で共有される必要がある場合は、この手法は使えません。
3. インスタンスの再利用とリセット
ObservableObjectがアプリ全体の状態(例: @EnvironmentObject)として存在し、インスタンスを破棄せずにデータだけをリセットしたい場合に適用されます。
手法:init時にリセットロジックを実行
ObservableObject側でデータをロードする前に、古いデータが存在しないことを確認するためのロジックを実行します。
|
1 2 3 4 5 6 7 8 9 10 11 |
class APIManager: ObservableObject { @Published var apiResults: [String] = [] func fetchData() { // ロード開始前に既存のデータをリセット apiResults = [] // ... 実際のAPI呼び出しロジック ... // ... データ格納 ... } } |
メリット:
- 常に最新のデータのみが表示されることが保証されます。
欠点:
fetchDataを呼び出すタイミングがずれると、データが一時的に空になる瞬間が発生します。一般的には、上記1番のonDisappearによる明示的なクリアが、Viewのライフサイクルの副作用を処理するうえで最も意図が伝わりやすいです。

コメント