ネットワークロジックが状態を持つ
API通信を行うアプリを作っている時にAIさんに指摘されたことをメモとして残します。
今回はSwiftUIでしたが恐らくクラスが使える言語で共通して言えることかと。
簡単に結論を書くと【ネットワークを行うクラスに他が依存するデータを保持するのは危険】ということ。
以下GPTさんの解説
🧠「ネットワークロジックが状態を持つ」とは?
たとえば次のような構成です:
|
1 2 3 4 5 6 7 |
@MainActor class GetOpenWeatherMapAPI: ObservableObject { @Published var token: String = "" @Published var weather: String = "" ... func getWeather() async { ... } } |
このように「ネットワーク処理を行うクラス」が @Published を持ち、
状態(トークン・結果・フラグ)を内部に保持している状態を指します。
⚠️ 問題点(=危険性・不具合リスク)
① 非同期処理の途中でViewが破棄される
SwiftUIではViewが頻繁に再生成されます。
その結果、ObservableObject のインスタンスが
再作成されたり破棄されたりすることがあります。
→ その時にネットワークタスクが生き残っていると、
- メモリリーク
- MainActorへの不正アクセス
- 競合(2重API呼び出し)
などが起こりやすくなります。
② @Published は UI更新をトリガーする
つまりAPIクラスが@Publishedを持つと、
内部的に「UIを更新する責任」まで負ってしまいます。
→ 結果、「ロジック」と「UI更新」が密結合になり、
再利用しにくくなる(テストも困難)。
③ Previewがクラッシュする原因にも
Xcodeプレビューは「ネットワークアクセス禁止」かつ
MainActorのスレッド管理が本番と違います。
@Published を持つAPIクラスで .task { await api.getWeather() } を書くと、
プレビュー中に非同期更新が発生 →
MainActor更新タイミングがずれてクラッシュすることがあります
✅ 解決策:「ネットワークロジックを純粋化」
これが「状態を持たせず、戻り値で返す」構成です。
|
1 2 3 4 5 6 |
class APIClient { func fetchWeather(...) async throws -> Weather { ... return weather } } |
これなら:
- 呼び出し時にだけ存在する
- 内部状態を持たない(スレッド安全)
- UI更新を担当しない
→ 「1回の呼び出し=1つの結果」で済む、非常にクリーンな構成になります。
💡 ViewModelを介して状態を保持すれば十分
@Published は ViewModelの責任範囲 に閉じ込めるのが鉄則です。
APIはあくまで“外部との通信担当”、
ViewModelが“結果をUIに渡す”立ち位置ですね。
ちなみに問題がある状態でAPI通信の発火条件をViewが表示された時にしていたらそこから画面遷移をした時に異常な回数API通信を行っていました。
Viewの更新頻度の高さがわかりました。


コメント