Skip to content

Instantly share code, notes, and snippets.

@BrentMifsud
Created September 7, 2025 22:31
Show Gist options
  • Save BrentMifsud/60ad63113934e5f73b89dd7e60f3dada to your computer and use it in GitHub Desktop.
Save BrentMifsud/60ad63113934e5f73b89dd7e60f3dada to your computer and use it in GitHub Desktop.
Convenience modifier for showing an alert based on a LocalizedError binding
/// 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