🪣

Appで取得したエラーを別Viewに伝播するシンプルなパターン

に公開

概要

  • Appで受け取ったエラーを別の画面でアラート表示したいというケースに対して、@Observableな状態クラスを 1 つ用意し、Appがそれを保持・更新し、View側は@Environmentから監視するパターンを考える

全体方針

  • アプリ全体のエラー状態を表すクラスAppErrorState@Observableで定義する
  • AppがAppErrorStateを保持し、.environment(appErrorState)で全Viewに渡す
  • 各Viewは@Environment(AppErrorState.self)で状態を読み取り、.onChange(of:)などで変化を検知してアラートを出す
    • アラートを閉じるタイミングでlastError = .noneに戻す

エラー状態クラスAppErrorState

import Observation

/// アプリ全体で共有するエラー状態。
@Observable
final class AppErrorState {
    /// 直近のエラー種別。
    enum LastError: Equatable {
        /// エラーなし(初期状態/ユーザー確認後のリセット状態)
        case none
        /// メッセージを伴うエラー
        case message(String)
    }

    /// 直近のエラー。
    ///
    /// View 側はこの値を監視してアラート表示などを行う。
    /// アラート表示後は `.none` に戻す運用を想定。
    var lastError: LastError = .none
}

Appで保持してEnvironment に流す

import SwiftUI

@main
struct ExampleApp: App {
    /// アプリ全体のエラー状態
    private let appErrorState = AppErrorState()

    private let sampleUseCase = SampleUseCase()

    var body: some Scene {
        WindowGroup {
            RootView()
                // AppErrorStateをEnvironmentに注入する
                .environment(appErrorState)
                .task {
                    // 起動時に何か同期処理を行う
                    await performInitialSync()
                }
        }
    }

    private func performInitialSync() async {
        do {
            try await sampleUseCase.run()
        } catch {
            await MainActor.run {
                // エラーを受けてAppErrorStateを更新
                appErrorState.lastError = .message("初期同期に失敗しました。時間をおいて再度お試しください。")
            }
        }
    }
}

View 側で監視してアラート表示

import SwiftUI

struct RootView: View {
    /// AppErrorStateをEnvironmentから取得
    @Environment(AppErrorState.self) private var appErrorState

    /// この画面でのエラーアラート表示状態
    @State private var isShowingErrorAlert = false
    @State private var errorMessage = ""

    var body: some View {
        NavigationStack {
            VStack(spacing: 16) {
                Text("Root View")
                    .font(.title2)

                NavigationLink("詳細画面へ") {
                    DetailView()
                }
            }
            .padding()
            .task {
                // 起動時にすでにエラーがあれば一度だけ拾う
                handle(lastError: appErrorState.lastError)
            }
            .onChange(of: appErrorState.lastError) { _, newValue in
                // 以降の変化を監視
                handle(lastError: newValue)
            }
            .alert("エラー", isPresented: $isShowingErrorAlert) {
                Button("OK") {
                    // アラートを閉じたタイミングでリセット
                    appErrorState.lastError = .none
                }
            } message: {
                Text(errorMessage)
            }
        }
    }

    /// AppErrorState の内容に応じてアラート表示を制御する。
    ///
    /// - Parameter lastError: 直近のエラー状態。
    private func handle(lastError: AppErrorState.LastError) {
        switch lastError {
        case .none:
            break
        case .message(let message):
            errorMessage = message
            isShowingErrorAlert = true
        }
    }
}

Discussion