128 lines
3.3 KiB
Swift
128 lines
3.3 KiB
Swift
import SwiftUI
|
|
|
|
// https://www.ralfebert.com/swiftui/generic-error-handling/
|
|
|
|
@MainActor
|
|
public final class ErrorHandler: ObservableObject {
|
|
let defaultTitle: String
|
|
|
|
let dismissTitle: String
|
|
|
|
let errorDescription: (Error) -> String
|
|
|
|
private let beforeAlert: (String) -> Void
|
|
|
|
@Published
|
|
fileprivate var currentAlert: ErrorAlert?
|
|
|
|
@Published
|
|
fileprivate var isPresented = false
|
|
|
|
var currentTitle: String {
|
|
currentAlert?.title ?? defaultTitle
|
|
}
|
|
|
|
public init(
|
|
defaultTitle: String,
|
|
dismissTitle: String,
|
|
errorDescription: @escaping (Error) -> String,
|
|
beforeAlert: @escaping (String) -> Void
|
|
) {
|
|
self.defaultTitle = defaultTitle
|
|
self.dismissTitle = dismissTitle
|
|
self.errorDescription = errorDescription
|
|
self.beforeAlert = beforeAlert
|
|
}
|
|
|
|
public func handle(
|
|
_ error: Error,
|
|
title: String? = nil,
|
|
message: String? = nil,
|
|
messageSeparator: String = " ",
|
|
onDismiss: (() -> Void)? = nil
|
|
) {
|
|
let composedMessage = [message, errorDescription(error)]
|
|
.compactMap { $0 }
|
|
.joined(separator: messageSeparator)
|
|
beforeAlert(composedMessage)
|
|
|
|
currentAlert = ErrorAlert(
|
|
title: title,
|
|
message: composedMessage,
|
|
dismissAction: onDismiss
|
|
)
|
|
enableLater {
|
|
self.isPresented = $0
|
|
}
|
|
}
|
|
|
|
public func handle(title: String, message: String, onDismiss: (() -> Void)? = nil) {
|
|
currentAlert = ErrorAlert(
|
|
title: title,
|
|
message: message,
|
|
dismissAction: onDismiss
|
|
)
|
|
enableLater {
|
|
self.isPresented = $0
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ErrorAlert: Identifiable {
|
|
let id = UUID()
|
|
|
|
let title: String?
|
|
|
|
let message: String
|
|
|
|
let dismissAction: (() -> Void)?
|
|
}
|
|
|
|
// MARK: - Modifier
|
|
|
|
extension View {
|
|
public func withErrorHandler(_ errorHandler: ErrorHandler) -> some View {
|
|
modifier(HandleErrorsByShowingAlertViewModifier(
|
|
errorHandler: errorHandler
|
|
))
|
|
}
|
|
}
|
|
|
|
private struct HandleErrorsByShowingAlertViewModifier: ViewModifier {
|
|
|
|
@ObservedObject
|
|
var errorHandler: ErrorHandler
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
// Applying the alert for error handling using a background element
|
|
// is a workaround, if the alert would be applied directly,
|
|
// other .alert modifiers inside of content would not work anymore
|
|
.background(
|
|
EmptyView()
|
|
.alert(
|
|
errorHandler.currentTitle,
|
|
isPresented: $errorHandler.isPresented,
|
|
presenting: errorHandler.currentAlert
|
|
) { alert in
|
|
Button(role: .cancel) {
|
|
alert.dismissAction?()
|
|
} label: {
|
|
Text(errorHandler.dismissTitle)
|
|
}
|
|
} message: { alert in
|
|
Text(alert.message.withTrailingDot)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
var withTrailingDot: String {
|
|
guard !hasSuffix(".") else {
|
|
return self
|
|
}
|
|
return "\(self)."
|
|
}
|
|
}
|