mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-10-27 11:15:08 +01:00
173 lines
4.9 KiB
Swift
173 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
enum ErrorCategory: Equatable {
|
|
case nonRecoverable
|
|
case recoverable(recoveryOption: RecoveryOption)
|
|
case requiresSignout
|
|
}
|
|
|
|
protocol CategorizedError: Error {
|
|
var category: ErrorCategory { get }
|
|
}
|
|
|
|
extension Error {
|
|
func resolveCategory() -> ErrorCategory {
|
|
guard let categorized = self as? CategorizedError else {
|
|
return .nonRecoverable
|
|
}
|
|
|
|
return categorized.category
|
|
}
|
|
}
|
|
|
|
struct RecoveryOption: Identifiable, Equatable, CustomStringConvertible {
|
|
let id: String
|
|
let description: String
|
|
|
|
init(id: String, description: String) {
|
|
self.id = id
|
|
self.description = description
|
|
}
|
|
}
|
|
|
|
extension RecoveryOption {
|
|
static let retry = RecoveryOption(id: "com.xcodesorg.Xcodes.retry", description: "Retry")
|
|
}
|
|
|
|
@MainActor
|
|
protocol ErrorHandler: Sendable {
|
|
func handle<T: View>(
|
|
_ error: Binding<Error?>,
|
|
in view: T,
|
|
recoveryHandler: @escaping (RecoveryOption) -> Void,
|
|
signOutHandler: @escaping () -> Void
|
|
) -> AnyView
|
|
}
|
|
|
|
struct AlertErrorHandler: ErrorHandler {
|
|
private let id = UUID()
|
|
|
|
func handle<T: View>(
|
|
_ error: Binding<Error?>,
|
|
in view: T,
|
|
recoveryHandler: @escaping (RecoveryOption) -> Void,
|
|
signOutHandler: @escaping () -> Void
|
|
) -> AnyView {
|
|
guard error.wrappedValue?.resolveCategory() != .requiresSignout else {
|
|
signOutHandler()
|
|
return AnyView(view)
|
|
}
|
|
|
|
let binding = Binding(
|
|
get: {
|
|
error.wrappedValue.map {
|
|
Presentation(
|
|
id: id,
|
|
error: $0,
|
|
recoveryHandler: { recoveryOption in
|
|
DispatchQueue.main.async {
|
|
recoveryHandler(recoveryOption)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
},
|
|
set: { possiblePresentation in
|
|
if possiblePresentation == nil {
|
|
error.wrappedValue = nil
|
|
}
|
|
}
|
|
)
|
|
|
|
return AnyView(view.alert(item: binding, content: makeAlert))
|
|
}
|
|
}
|
|
|
|
private extension AlertErrorHandler {
|
|
struct Presentation: Identifiable {
|
|
let id: UUID
|
|
let error: Error
|
|
let recoveryHandler: (RecoveryOption) -> Void
|
|
}
|
|
|
|
func makeAlert(for presentation: Presentation) -> Alert {
|
|
let error = presentation.error
|
|
|
|
switch error.resolveCategory() {
|
|
case let .recoverable(recoveryOption):
|
|
return Alert(
|
|
title: Text("An error occurred"),
|
|
message: Text(error.localizedDescription),
|
|
primaryButton: .default(Text("Dismiss")),
|
|
secondaryButton: .default(
|
|
Text(recoveryOption.description),
|
|
action: { presentation.recoveryHandler(recoveryOption) }
|
|
)
|
|
)
|
|
case .nonRecoverable:
|
|
return Alert(
|
|
title: Text("An error occurred"),
|
|
message: Text(error.localizedDescription),
|
|
dismissButton: .default(Text("Dismiss"))
|
|
)
|
|
case .requiresSignout:
|
|
assertionFailure("Should have signed out")
|
|
return Alert(title: Text("Signing out..."))
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ErrorHandlerEnvironmentKey: EnvironmentKey {
|
|
static let defaultValue: any ErrorHandler = AlertErrorHandler()
|
|
}
|
|
|
|
private struct SignOutHandlerEnvironmentKey: EnvironmentKey {
|
|
static let defaultValue: @MainActor @Sendable () -> Void = {}
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var errorHandler: any ErrorHandler {
|
|
get { self[ErrorHandlerEnvironmentKey.self] }
|
|
set { self[ErrorHandlerEnvironmentKey.self] = newValue }
|
|
}
|
|
|
|
var signOutHandler: @MainActor @Sendable () -> Void {
|
|
get { self[SignOutHandlerEnvironmentKey.self] }
|
|
set { self[SignOutHandlerEnvironmentKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
struct ErrorEmittingViewModifier: ViewModifier {
|
|
@SwiftUI.Environment(\.errorHandler) private var errorHandler: any ErrorHandler
|
|
@SwiftUI.Environment(\.signOutHandler) private var signOutHandler: @MainActor @Sendable () -> Void
|
|
|
|
var error: Binding<Error?>
|
|
var recoveryHandler: (RecoveryOption) -> Void
|
|
|
|
func body(content: Content) -> some View {
|
|
errorHandler.handle(
|
|
error,
|
|
in: content,
|
|
recoveryHandler: recoveryHandler,
|
|
signOutHandler: signOutHandler
|
|
)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func handlingErrors(using handler: any ErrorHandler) -> some View {
|
|
environment(\.errorHandler, handler)
|
|
}
|
|
|
|
func emittingError(
|
|
_ error: Binding<Error?>,
|
|
recoveryHandler: @escaping (RecoveryOption) -> Void
|
|
) -> some View {
|
|
modifier(ErrorEmittingViewModifier(
|
|
error: error,
|
|
recoveryHandler: recoveryHandler
|
|
))
|
|
}
|
|
}
|