Created
September 7, 2025 22:31
-
-
Save BrentMifsud/60ad63113934e5f73b89dd7e60f3dada to your computer and use it in GitHub Desktop.
Convenience modifier for showing an alert based on a LocalizedError binding
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// A reusable SwiftUI `ViewModifier` that presents a standard `Alert` for any `LocalizedError`-conforming error type, | |
/// while letting callers customize the alert's actions and message content. | |
/// | |
/// This modifier bridges SwiftUI's `alert(isPresented:error:actions:message:)` API into a convenient, type-safe wrapper | |
/// that is driven by an optional error binding. When the bound error becomes non-`nil`, the alert is presented; when the | |
/// alert is dismissed, the modifier automatically resets the bound error to `nil`. | |
/// | |
/// Type Parameters: | |
/// - E: The specific error type to present. Must conform to `LocalizedError` to integrate with SwiftUI's error alert API. | |
/// - Actions: The `View` type used to render the alert's actions (e.g., buttons). | |
/// - Message: The `View` type used to render the alert's descriptive message. | |
/// | |
/// Behavior: | |
/// - The modifier derives an internal `Binding<Bool>` from the provided `Binding<E?>`. | |
/// - Setting `isPresented` to `false` (e.g., dismissing the alert) clears the bound error (`error = nil`), ensuring a | |
/// single source of truth and preventing stale presentation state. | |
/// - The alert's title is automatically derived from the error via SwiftUI's error alert initializer; callers supply | |
/// custom actions and message views through the provided closures. | |
/// | |
/// Parameters: | |
/// - alertError: A binding to an optional error. When non-`nil`, the alert is shown. When the alert is dismissed, | |
/// this binding is reset to `nil`. | |
/// - actions: A `@MainActor` view-building closure that receives the current error and returns the alert's actions | |
/// (typically one or more `Button`s). | |
/// - message: A `@MainActor` view-building closure that receives the current error and returns the alert's message view. | |
/// | |
/// Concurrency: | |
/// - Both `actions` and `message` closures are annotated with `@MainActor` because they construct UI views and are | |
/// invoked as part of SwiftUI's rendering pipeline on the main thread. | |
/// | |
/// Usage: | |
/// - Prefer using the convenience `View.alert(error:actions:message:)` extension that applies this modifier, | |
/// to keep call sites concise and expressive. | |
/// | |
/// Example: | |
/// ```swift | |
/// @State private var error: MyError? | |
/// | |
/// SomeView() | |
/// .modifier( | |
/// ErrorAlertModifier( | |
/// alertError: $error, | |
/// actions: { _ in Button("OK") { error = nil } }, | |
/// message: { error in Text(error.errorDescription ?? "Something went wrong.") } | |
/// ) | |
/// ) | |
/// ``` | |
/// | |
/// Notes: | |
/// - Ensure your error type provides meaningful `LocalizedError` descriptions to improve the alert's title and message. | |
/// - If you need a default action or message, you can wrap this modifier in higher-level helpers or provide overloads. | |
struct ErrorAlertModifier<E: LocalizedError, A: View, M: View>: ViewModifier { | |
private var alertBinding: Binding<Bool> { | |
Binding { | |
alertError != nil | |
} set: { isPresented in | |
if !isPresented { | |
alertError = nil | |
} | |
} | |
} | |
@Binding var alertError: E? | |
@ViewBuilder var actions: @MainActor (E) -> A | |
@ViewBuilder var message: @MainActor (E) -> M | |
func body(content: Content) -> some View { | |
content | |
.alert( | |
isPresented: alertBinding, | |
error: alertError, | |
actions: actions, | |
message: message | |
) | |
} | |
} | |
extension View { | |
/// Presents a SwiftUI alert driven by an optional `LocalizedError`, with fully customizable actions and message content. | |
/// | |
/// This convenience overload wraps SwiftUI’s `alert(isPresented:error:actions:message:)` initializer and binds its | |
/// presentation directly to the presence of an error. When the bound `error` becomes non-`nil`, the alert is shown. | |
/// When the alert is dismissed, the underlying modifier clears the error (sets it back to `nil`) to keep state in sync. | |
/// | |
/// - Parameters: | |
/// - error: A binding to an optional error. If the value is non-`nil`, the alert is presented. Dismissing the alert | |
/// resets this binding to `nil`. | |
/// - actions: A `@MainActor` view-builder closure that receives the current `Error` and returns the alert’s actions | |
/// (e.g., one or more `Button`s). | |
/// - message: A `@MainActor` view-builder closure that receives the current `Error` and returns the alert’s message | |
/// view (e.g., descriptive text). | |
/// | |
/// - Returns: A view that conditionally presents an alert based on the provided error binding. | |
/// | |
/// - Important: The `Error` generic must conform to `LocalizedError` so SwiftUI can derive a localized title and other | |
/// user-facing descriptions for the alert. | |
/// | |
/// - Discussion: | |
/// - This API centralizes error-driven alert presentation and ensures a single source of truth for the alert’s | |
/// visibility by deriving it from the error’s presence. | |
/// - Use this when you want to customize both the actions and message while relying on the error to drive presentation. | |
/// | |
/// - Example: | |
/// ```swift | |
/// @State private var error: MyError? | |
/// | |
/// SomeView() | |
/// .alert(error: $error) { err in | |
/// Button("OK") { error = nil } | |
/// } message: { err in | |
/// Text(err.failureReason ?? err.errorDescription ?? "Something went wrong.") | |
/// } | |
/// ``` | |
func alert<E: LocalizedError, A: View, M: View>( | |
error: Binding<E?>, | |
@ViewBuilder actions: @escaping @MainActor (E) -> A, | |
@ViewBuilder message: @escaping @MainActor (E) -> M | |
) -> some View { | |
modifier(ErrorAlertModifier(alertError: error, actions: actions, message: message)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment