ソフトウェア設計

Swift enumで状態機械(FSM)を作る:ステートパターンなしで状態を管理する方法

iOSアプリを作っていると、いつか必ず遭遇します。1つの画面にローディング中、成功、失敗、データなしの状態が混在する瞬間です。

読了 5 分
Swift enumで状態機械(FSM)を作る:ステートパターンなしで状態を管理する方法のカバー画像

iOSアプリを作っていると、いつか必ず遭遇します。1つの画面にローディング中、成功、失敗、データなしの状態が混在する瞬間です。

isLoadinghasErrorisEmptyのようなBool変数を大量に作って管理すると、変数が増えるほど地獄になります。

ローディング中なのにエラーもtrueの状態。論理的にはあり得なくても、コード上では簡単に作れてしまいます。

結論から言うと、こうした状況はステートパターン(State Pattern)を使わず、Swift enum 1つで状態機械(FSM)を作ると、ずっとすっきり整理できます。

今日は、コードと一緒にBool地獄から抜け出す方法を見ていきます。


状態機械(FSM)とは?

難しく考えなくて大丈夫です。

状態機械、FSM(Finite State Machine)とは、「現在あり得る状態数が決まっていて、定められたルールに従ってのみ状態が変わる」という考え方です。

信号機を思い浮かべると簡単です。

青 → 黄 → 赤。この順番でだけ変わり、青からいきなり赤にはなりません。

アプリの画面も同じです。로딩성공または로딩실패へ進み、성공であり同時に로딩でもある状態は存在してはいけません。

しかしBool変数を複数使うと、存在してはいけない状態もコード上で作れてしまいます。

そこを防いでくれるのがenumです。


Swift enumで状態機械を作る方法

Swiftのenumは、単なる定数の列挙ではありません。

各caseが値(associated value)を持てることがポイントです。1つのenumに状態とデータを一緒に保持できます。

画面の状態を次のように定義しました。

enum LoadState {
    case idle              // まだ何もしていない
    case loading           // 読み込み中
    case loaded([Item])    // 成功、データ付き
    case failed(Error)     // 失敗、エラー付き
}

見ると、loadedはアイテム配列を、failedはエラーを一緒に保持しています。

成功なら必ずデータがあり、失敗なら必ずエラーがあります。データのない成功のような奇妙な状態は、そもそも作れません。

ビューでは、この1つの状態だけを見て画面を描画すればよいのです。

switch state {
case .idle:    EmptyView()
case .loading: ProgressView()
case .loaded(let items): ItemList(items)
case .failed(let error): ErrorView(error)
}

switchがすべてのcaseの処理を強制するため、後から状態を追加しても、コンパイラーが未処理箇所をすぐ指摘してくれます。

これが大きな利点です。状態の抜け漏れを人ではなくコンパイラーが検出してくれます。

成功にはデータ、失敗にはエラーを一緒に持たせるのがポイント
成功にはデータ、失敗にはエラーを一緒に持たせるのがポイント

ステートパターンなしでも大丈夫?

オブジェクト指向の教科書では、状態管理は通常ステートパターンで学びます。

状態ごとにクラスを作ってプロトコルでまとめ、遷移ロジックを各クラスに入れる方法です。

正直に言うと、画面状態の管理には大げさなことが少なくありません。

2つの方法を簡単に比較しました。(2026年、私の実務経験に基づきます)

項目 enum FSM ステートパターン
ファイル/型の数 enum 1つ 状態数分のクラス
状態の処理漏れ防止 switchを強制し、コンパイラーが検出 人が自分で確認する必要がある
データの同梱 associated valueで自然に扱える プロパティとして別管理
適した規模 3~7状態程度 状態ごとのロジックが非常に複雑な場合

状態数が適度で、状態ごとのロジックが重くないなら、enum FSMのほうがずっと軽量で安全です。

逆に、各状態に数十行規模の複雑な動作が付くなら、ステートパターンや別オブジェクトへの分離を検討するとよいでしょう。

どちらか一方が常に正解というわけではありません。


状態遷移はどう管理する?

enumを作るだけでは、どの状態にも勝手に変わってしまうのではと思うかもしれません。

よい質問です。だから私は遷移ルールを1つの関数にまとめます。

mutating func fetch() {
    guard case .idle = self else { return } // idleidleのときだけ開始
    self = .loading
}

guard caseで「現在idleのときだけローディングへ進め」と明示しています。

遷移条件を1か所に置けば、信号機のように決められた道だけを状態が流れます。

そのため、後からコードを見返しても「この状態はどこからどこへ進めるのか」が一目で分かります。

決められた道だけを流れる状態たち
決められた道だけを流れる状態たち

Q. 状態が5つを超えたら?

それでもenumは十分対応できます。ただし遷移ルールが複雑になったら、遷移関数を状態ごとに細かく分けて整理するのがおすすめです。

Q. SwiftUIと相性はいい?

とても相性がいいです。enumの状態を@State@Publishedに置き、switchでビューを描画すれば、状態と画面がぴったり連動します。


最初はBoolが数個あれば十分に見えても、状態が絡み始めると、結局enum FSMに戻ってきます。

画面状態の管理に悩んでいるなら、今日見たenumから少しずつ置き換えてみてください。思ったより早く楽になります。

あわせて読みたい