はじめに
以前書いたSwiftUI向けエラーハンドラーを、1つのファイルにしていたのですが整理するためにそれとなくディレクトリ分けしました。
階層
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
// 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
// MARK: - Protocols/ErrorLogging.swift
protocol ErrorLogging {
func log(_ error: Error, file: String, line: Int, function: String)
}
Logging
Logging/DevelopmentErrorLogger.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
// 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
// MARK: - Types/ErrorDisplayType.swift
enum ErrorDisplayType {
case alert(ErrorAction)
case inline(String)
}
Types/ErrorAction.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
// 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
// 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
// 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
// MARK: - Extensions/View+ErrorAlert.swift
extension View {
func errorAlert(errorHandler: ErrorHandler) -> some View {
modifier(ErrorAlert(errorHandler: errorHandler))
}
}
ErrorHandler
ErrorHandler.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
}
}