I have a simple view where you can show an alert and then click the "Yes" button to dismiss it. The issue I'm having is that when tapping "Yes", the print()
statement fires only after the alert is fully dismissed, not immediately. I'm assuming this is caused by the default animation but not certain.
How can I make it fire the closure immediately upon tapping "Yes"?
import SwiftUI
struct ContentView: View {
@State private var show = false
var body: some View {
VStack {
Button("Show Alert") {
show = true
}
}
.alert("Cancel exposure?", isPresented: $show) {
Button("Yes", role: .destructive) {
print("Dismissing") // Should show immediately when tapping "Yes"
}
Button("No", role: .cancel) { }
}
}
}
#Preview {
ContentView()
}
You can make it fire the closure immediately upon tapping "Yes" by changing the code to use a custom alert implementation.
The Apple-supplied .alert
view modifier only calls its actions after it has completed its animations and so it is impossible to tweak it to run the actions prior to the animations completing. This is in contrast to its official documentation which claims otherwise.
The rest of this answer explains the above statement and provides a code example workaround.
The documentation, https://developer.apple.com/documentation/swiftui/view/alert(_:ispresented:presenting:actions:message:)-8584l
says
... All actions in an alert dismiss the alert after the action runs.
However, if we use a breakpoint, we can see:
From backtrace level 13, highlighted, we can clearly see the code is doing post-animation housekeeping. In particular it is calling our print statement.
This is clearly a contradiction when compared to the documentation.
However, in a "reactive" programming environment, such strong guarantees can usually not be offered. You just can't rely on the dynamics of the UI being updated or refreshed in relation to your callback code. So I don't think raising a Feedback with Apple will get you very far.
This leaves only the custom Alert approach that others have suggested.
An example which shows your desired behaviour, based on adapted code from Benzy Neez's StackOverflow answer is:
import SwiftUI
struct ContentView: View {
@State var show = false
private var actionThenAnimateAlert: some View {
VStack(spacing: 0) {
VStack(spacing: 6) {
Text("Cancel exposure?")
.font(.headline)
}
.padding()
.frame(minHeight: 80)
Divider()
HStack(spacing: 0) {
Button("No", role: .cancel) {
show = false
}
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
Divider()
Button("Yes", role: .destructive) {
print("Dismissing")
show = false
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderless)
.frame(maxHeight: 44)
}
.frame(maxWidth: 270)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(.background)
}
}
var body: some View {
ZStack {
VStack {
Button("Show Alert") {
show = true
}
}
if show {
Color.black
.opacity(0.2)
.ignoresSafeArea()
.onTapGesture { show = false }
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
actionThenAnimateAlert
.transition(
.scale(0.8).combined(with: .opacity)
.animation(.spring(duration: 0.25))
)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
#Preview {
ContentView()
}
Here is a snapshot of it working, where Dismiss is printed before the alert is dismissed