passepartout-apple/Library/Sources/CommonUtils/Business/ErrorHandler.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)."
}
}