View間共通変数

@Observable(新しい書き方)

✔ @EnvironmentObject は 将来的に非推奨方向

macOS 14 / iOS 17 の Observation フレームワークの登場により、

  • ObservableObject
  • @Published
  • @EnvironmentObject

は だんだん使わなくなる方向

新しい書き方

① 共有設定クラス

② App 起動時に environment に注入する

③ 各 View からはこう呼ぶ

✔ 新パターンは「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 や バインディング例

@EnvironmentObject(古い)

全View間で使えるグローバル変数的なものです。
変数に変更が掛かるとすべてのViewで連動して変更されます。
注意点として、最上位のViewに.environmentObject()で渡してあげる事とEnvironmentObjectをインスタンス化したらすべてのプレビューの所でもインスタンス化してあげる事。
あとはグローバル変数の危険性を知っておくと良いかもしれません、似たようなものなので。

@EnvironmentObject の役割

@EnvironmentObject は、Viewとその直接的なViewロジックにおいて、データの受け渡しを簡略化するためにSwiftUIが提供する機能です。

  • View階層の深くにあるViewでも、プロパティとして宣言するだけで簡単にデータにアクセスできるメリットがあります。
  • 主にViewを再構築するためのデータソースとして使用されます。

@StateObject

こちらはインスタンス化したViewの中でのみ状態変化を保持します。(View毎でインスタンス化すると別物扱いになります)
なので他のViewで変数の状態変化を共有させたい時は引数で渡す必要があります。

@StateObject と #Preview の関係

1. @StateObject の役割(生成と所有)

@StateObject は、ビューが初めて初期化されるときに、その場で新しいObservableObjectのインスタンスを生成します。

このため、#Preview でこのビューをインスタンス化するときも、APIResultView が内部で APIManager() を生成するため、外部から APIManager のインスタンスを注入(提供)する必要がありません。

2. #Preview の記述

@StateObject を使用する場合の #Preview の記述は、非常にシンプルになります。

3. @EnvironmentObject の場合(対比)

一方で、@EnvironmentObject を使用する場合、ビューは外部からインスタンスが提供されることを期待します。

このため、#Preview でもその期待に応える必要があり、エラーを避けるために .environmentObject() を使ってインスタンスを「注入」する必要がありました。

注意点

SwiftUI管轄外、Viewを使わないクラス等では直接インスタンス化出来ない。

エラー指摘されずクラッシュします。
通常のクラスにObservableObjectを渡したい時は明示的に渡す。

GPTさん曰く
「SwiftUIって、@EnvironmentObjectObservableObjectの依存関係が裏で勝手に解決されるから、
「ただクラスをnewしただけで使える」と思いがちなんですよね。
でも@EnvironmentObjectが付いたクラスは基本「SwiftUI側がインスタンスを注入する前提」なので、
自前で init() しても nil のまま… → そこでクラッシュ、というのがよくあるパターンです💦」

Simulatorは大丈夫でもPreviewがクラッシュしやすい

非同期に処理結果をViewに反映している場合、その処理の順番が実機での順番とPreviewでの順番が違うためSwiftUI が環境をまだ用意していない状態でアクセスしてクラッシュすることがある。

自分は他のViewと共通して持っていたObservableObjectのデータを使ってAPI通信をし、結果をViewに表示するViewに渡していた為この問題が起きた。
PreviewはそのView単体表示させる為他のViewのObservableObjectデータ内容を知ることができない。

回避策はPreviewにダミーデータを渡す。

補足:なぜプレビューだけ落ちるのか
・プレビューはアプリ全体じゃなく「View単体」を再現するだけなので
・@EnvironmentObject の「親View」が存在しない
・SwiftUIプレビューの裏では App の @main や Scene が動いていない
・非同期処理 (async) や .task の呼び出しが開始前に nilアクセスを起こす
・Simulatorでは App 全体が立ち上がるため正常

ObservableObject のデータクリア方法(View変える時とか)

1. データのクリアをViewのライフサイクルに結合する (推奨)

最も一般的で、SwiftUIのライフサイクルに沿った手法です。データを保持しているObservableObjectとは独立して、Viewが消えるタイミングでクリア関数を呼び出します。

手法:onDisappear の利用

API結果を表示するViewに .onDisappear モディファイアを追加し、その中でObservableObjectに用意したクリア用メソッドを呼び出します。

メリット:

  • 明確な意図: いつデータをクリアするかがコードで明確になります。
  • シンプル: ObservableObject側にロジックを追加するだけで済みます。

2. 新しいインスタンスを生成する (StateObjectの利用)

表示Viewに遷移するたびにデータがクリアされた状態から始まるのが望ましい場合、@StateObjectを使用してViewのライフサイクルとObservableObjectのインスタンスのライフサイクルを結合します。

手法:@StateObject を使用

@StateObjectで宣言されたインスタンスは、そのViewが生存している間だけメモリに保持されます。Viewが破棄されると、@StateObjectのインスタンスも一緒に破棄され、その中のデータは解放されます。

メリット:

  • 自動クリーンアップ: clearResults() のような手動の呼び出しが不要で、Viewが閉じるとインスタンスごと解放されます。
  • クリーンな状態: 遷移するたびに、常に初期状態からデータがロードされます。

デメリット:

  • データが別のViewやアプリ全体で共有される必要がある場合は、この手法は使えません。

3. インスタンスの再利用とリセット

ObservableObjectがアプリ全体の状態(例: @EnvironmentObject)として存在し、インスタンスを破棄せずにデータだけをリセットしたい場合に適用されます。

手法:init時にリセットロジックを実行

ObservableObject側でデータをロードする前に、古いデータが存在しないことを確認するためのロジックを実行します。

メリット:

  • 常に最新のデータのみが表示されることが保証されます。

欠点:

  • fetchDataを呼び出すタイミングがずれると、データが一時的に空になる瞬間が発生します。一般的には、上記1番の onDisappearによる明示的なクリアが、Viewのライフサイクルの副作用を処理するうえで最も意図が伝わりやすいです。

コメント

タイトルとURLをコピーしました