SwiftUIエラーハンドラーのファイル整理

## はじめに
以前書いたSwiftUI向けエラーハンドラーを、１つのファイルにしていたのですが整理するためにそれとなくディレクトリ分けしました。

https://qiita.com/kazuyuki_11E0/items/df157d1057bbb8f88f4e

## 階層
```
ErrorHandler/
│
├── Entities/
│   └── AppError.swift
│
├── Protocols/
│   └── ErrorLogging.swift
│
├── Types/
│   ├── ErrorAction.swift
│   └── ErrorDisplayType.swift
│
├── Logger/
│   ├── DevelopmentErrorLogger.swift
│   └── ProductionErrorLogger.swift
│
├── Environment/
│   ├── ErrorHandlerKey.swift
│   └── NavigationPathKey.swift
│
├── Components/
│   └── ErrorAlert.swift
│
├── Extensions/
│   └── View+ErrorAlert.swift
│
└── ErrorHandler.swift
```


## Entities
### Entities/AppError.swift
```swift
// MARK: - Entities/AppError.swift
import Foundation

enum AppError: Error, Identifiable {
    var id: String { localizedDescription }
    
    case network(underlying: Error)
    case invalidResponse
    case noData
    case decodingError(underlying: Error)
    case invalidData(reason: String)
    case validation(reason: String)
    case unauthorized
    case forbidden
    case notFound
    case unknown(Error)
    case invalidCredentials
    
    var userMessage: String {
        switch self {
        case .network:
            return "通信エラーが発生しました。通信環境をご確認ください。"
        case .invalidResponse, .noData:
            return "データの取得に失敗しました。"
        case .decodingError:
            return "データの処理中にエラーが発生しました。"
        case .invalidData(let reason):
            return "不正なデータです: \(reason)"
        case .validation(let reason):
            return "入力内容に問題があります: \(reason)"
        case .unauthorized:
            return "認証が必要です。再度ログインしてください。"
        case .forbidden:
            return "この操作を実行する権限がありません。"
        case .notFound:
            return "お探しの情報が見つかりませんでした。"
        case .unknown:
            return "予期せぬエラーが発生しました。"
        case .invalidCredentials:
            return "メールアドレスもしくはパスワードが違います。"
        }
    }
}
```

## Protocols
### Protocols/ErrorLogging.swift
```swift
// MARK: - Protocols/ErrorLogging.swift
protocol ErrorLogging {
    func log(_ error: Error, file: String, line: Int, function: String)
}
```

## Logging

### Logging/DevelopmentErrorLogger.swift
```swift
// MARK: - Logging/DevelopmentErrorLogger.swift
class DevelopmentErrorLogger: ErrorLogging {
    func log(_ error: Error, file: String, line: Int, function: String) {
        print("🚨 Error occurred:")
        print("📝 File: \(file)")
        print("📍 Line: \(line)")
        print("⚡️ Function: \(function)")
        print("💥 Error: \(error)")
        
        if let appError = error as? AppError {
            print("📱 User Message: \(appError.userMessage)")
        }
    }
}
```

###  Logging/ProductionErrorLogger.swift

```swift
// MARK: - Logging/ProductionErrorLogger.swift
class ProductionErrorLogger: ErrorLogging {
    func log(_ error: Error, file: String, line: Int, function: String) {
        // Crashlyticsなどへのログ送信
        // Analytics.logError(error)
    }
}
```

## Types

### Types/ErrorDisplayType.swift
```swift
// MARK: - Types/ErrorDisplayType.swift
enum ErrorDisplayType {
    case alert(ErrorAction)
    case inline(String)
}
```

### Types/ErrorAction.swift
```swift
// MARK: - Types/ErrorAction.swift
enum ErrorAction {
    case retry(() async -> Void, buttonTitle: String = "再試行")
    case navigateBack(buttonTitle: String = "戻る")
    case dismiss(buttonTitle: String = "OK")
    
    var buttonTitle: String {
        switch self {
        case .retry(_, let title):
            return title
        case .navigateBack(let title):
            return title
        case .dismiss(let title):
            return title
        }
    }
}
```

## Enviroment
### Environment/NavigationPathKey.swift

```swift
// MARK: - Shared/Environment/NavigationPathKey.swift
import SwiftUI

private struct NavigationPathKey: EnvironmentKey {
    static let defaultValue: Binding<NavigationPath> = .constant(NavigationPath())
}

extension EnvironmentValues {
    var navigationPath: Binding<NavigationPath> {
        get { self[NavigationPathKey.self] }
        set { self[NavigationPathKey.self] = newValue }
    }
}
```

### Environment/ErrorHandlerKey.swift
```swift
// MARK: - Shared/Environment/ErrorHandlerKey.swift
private struct ErrorHandlerKey: EnvironmentKey {
    static let defaultValue = ErrorHandler()
}

extension EnvironmentValues {
    var errorHandler: ErrorHandler {
        get { self[ErrorHandlerKey.self] }
        set { self[ErrorHandlerKey.self] = newValue }
    }
}
```

## Components

### Components/ErrorAlert.swift
```swift
// MARK: - Components/ErrorAlert.swift
import SwiftUI

struct ErrorAlert: ViewModifier {
    @ObservedObject var errorHandler: ErrorHandler
    @Environment(\.navigationPath) private var navigationPath
    @Environment(\.dismiss) private var dismiss
    
    func body(content: Content) -> some View {
        content
            .alert("エラー",
                   isPresented: Binding(
                    get: { errorHandler.currentError != nil },
                    set: { if !$0 { handleDismiss() } }
                   ),
                   actions: {
                if let action = errorHandler.errorAction {
                    Button(action.buttonTitle) {
                        handleAction(action)
                    }
                }
            }, message: {
                if let error = errorHandler.currentError {
                    Text(error.userMessage)
                }
            })
    }
    
    private func handleAction(_ action: ErrorAction) {
        switch action {
        case .retry(let retryAction, _):
            Task {
                await retryAction()
                errorHandler.dismissError()
            }
        case .navigateBack:
            handleNavigateBack()
        case .dismiss:
            handleDismiss()
        }
    }
    
    private func handleNavigateBack() {
        errorHandler.dismissError()
        if navigationPath.wrappedValue.count > 0 {
            navigationPath.wrappedValue.removeLast()
        } else {
            dismiss()
        }
    }
    
    private func handleDismiss() {
        errorHandler.dismissError()
    }
}
```

## Extensions

### Extensions/View+ErrorAlert.swift
```swift
// MARK: - Extensions/View+ErrorAlert.swift
extension View {
    func errorAlert(errorHandler: ErrorHandler) -> some View {
        modifier(ErrorAlert(errorHandler: errorHandler))
    }
}
```

## ErrorHandler

### ErrorHandler.swift

```swift
// MARK: - Shared/ErrorHandler.swift
@MainActor
final class ErrorHandler: ObservableObject {
    @Published private(set) var currentError: AppError?
    @Published private(set) var errorAction: ErrorAction?
    @Published private(set) var inlineErrorMessage: String?
    
    private let logger: ErrorLogging
    
    init(logger: ErrorLogging? = nil) {
        self.logger = logger ?? {
            #if DEBUG
            return DevelopmentErrorLogger()
            #else
            return ProductionErrorLogger()
            #endif
        }()
    }
    
    func handle(
        _ error: Error,
        displayType: ErrorDisplayType,
        file: String = #file,
        line: Int = #line,
        function: String = #function
    ) {
        logger.log(error, file: file, line: line, function: function)
        
        currentError = (error as? AppError) ?? .unknown(error)
        
        switch displayType {
        case .alert(let action):
            errorAction = action
            inlineErrorMessage = nil
        case .inline(let message):
            inlineErrorMessage = message
            errorAction = nil
        }
    }
    
    func dismissError() {
        currentError = nil
        errorAction = nil
        inlineErrorMessage = nil
    }
}
```