Created
September 1, 2023 09:50
-
-
Save phillipcaudell/ecb32dc5a3915931d225a6cb8a67ac34 to your computer and use it in GitHub Desktop.
Present a confirmation dialogue from a swipeAction, contextMenu or other transient view.
This file contains 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
import SwiftUI | |
/// Describe a potentially destructive action to the user. | |
/// | |
/// A confirmation describes the action to the user and provides a mechanism for confirming the action in the | |
/// form of a `Button`. You present a confirmation by calling the environment's confirm action: | |
/// | |
/// ```swift | |
/// @Environment(\.confirm) private var confirm | |
/// | |
/// var body: some View { | |
/// Button { | |
/// confirm(deletion) | |
/// } label: { | |
/// Label("Delete", systemImage: "trash") | |
/// } | |
/// .tint(.red) | |
/// } | |
/// | |
/// var deletion: Confirmation { | |
/// Confirmation( | |
/// "Are you sure you want to delete?", | |
/// titleVisibility: .visible | |
/// ) { | |
/// Button(role: .destructive) { | |
/// delete() | |
/// } label: { | |
/// Text("Delete Forever") | |
/// } | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// You can also call confirm directly without the need for a confirmation instance: | |
/// | |
/// ```swift | |
/// @Environment(\.confirm) private var confirm | |
/// | |
/// var body: some View { | |
/// Button { | |
/// confirm("Are you sure?") { | |
/// Button(role: .destructive) { | |
/// delete() | |
/// } label: { | |
/// Text("Delete Forever") | |
/// } | |
/// } | |
/// } label: { | |
/// Label("Delete", systemImage: "trash") | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// You must install a `confirmationDialogue()` somewhere higher up in the view hierarchy that will | |
/// remain stable, such as a `List` or `NavigationStack`. | |
/// | |
/// ```swift | |
/// List { | |
/// ... | |
/// } | |
/// .confirmationDialog() | |
/// | |
public struct Confirmation { | |
let id = UUID() | |
let title: LocalizedStringKey | |
let titleVisibility: Visibility | |
// A little bit of type-erasure never hurt anybody | |
let message: AnyView | |
let actions: AnyView | |
} | |
extension Confirmation { | |
/// Creates a new confirmation instance. | |
public init<Actions, Message>( | |
_ title: LocalizedStringKey, | |
titleVisibility: Visibility = .automatic, | |
@ViewBuilder message: @escaping () -> Message, | |
@ViewBuilder actions: @escaping () -> Actions | |
) where Actions: View, Message: View { | |
self.title = title | |
self.titleVisibility = titleVisibility | |
self.actions = AnyView(actions()) | |
self.message = AnyView(message()) | |
} | |
/// Creates a new confirmation instance. | |
public init<Actions>( | |
_ title: LocalizedStringKey, | |
titleVisibility: Visibility = .automatic, | |
@ViewBuilder actions: @escaping () -> Actions | |
) where Actions: View { | |
self.init(title, titleVisibility: titleVisibility) { | |
EmptyView() | |
} actions: { | |
actions() | |
} | |
} | |
} | |
/// Handles confirmations in the environment. | |
public struct ConfirmationAction { | |
let handler: (Confirmation) -> Void | |
public func callAsFunction(_ confirmation: Confirmation) { | |
handler(confirmation) | |
} | |
public func callAsFunction<Actions>( | |
_ title: LocalizedStringKey, | |
titleVisibility: Visibility = .automatic, | |
@ViewBuilder actions: @escaping () -> Actions | |
) where Actions: View { | |
handler( | |
Confirmation( | |
title, | |
titleVisibility: titleVisibility, | |
actions: actions | |
) | |
) | |
} | |
} | |
struct ConfirmationDialogModifier: ViewModifier { | |
@State private var isPresented = false | |
@State private var confirmation: Confirmation? | |
func body(content: Content) -> some View { | |
content | |
.confirmationDialog( | |
confirmation?.title ?? "", | |
isPresented: $isPresented, | |
titleVisibility: confirmation?.titleVisibility ?? .automatic | |
) { | |
if let confirmation { | |
confirmation.actions | |
} | |
} message: { | |
if let confirmation { | |
confirmation.message | |
} | |
} | |
.environment(\.confirm, confirmAction) | |
} | |
var confirmAction: ConfirmationAction { | |
.init { | |
confirmation = $0 | |
isPresented = true | |
} | |
} | |
} | |
extension EnvironmentValues { | |
struct ConfirmationKey: EnvironmentKey { | |
static var defaultValue = ConfirmationAction { _ in } | |
} | |
/// Present a confirmation to the user. | |
public var confirm: ConfirmationAction { | |
get { self[ConfirmationKey.self] } | |
set { self[ConfirmationKey.self] = newValue } | |
} | |
} | |
extension View { | |
/// Installs a confirmation dialogue in the view. | |
public func confirmationDialog() -> some View { | |
self.modifier(ConfirmationDialogModifier()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you present a confirmation dialogue from a swipeAction or contextMenu nothing will happen as the view is removed before the dialogue is presented. Annoying, but it makes perfect sense.
Instead, delegate the confirmation to the environment. Now the Button isn't responsible for the presentation and you're left with a nice clean call site.
Here's a complete example: