0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

以前書いた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
    }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?