Search code examples
iosswiftswiftui

Run closure immediately after tapping button on .alert()


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()
}

Solution

  • 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.

    Apple Alert documentation and analysis

    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: Screen shot of Xcode showing a breakpoint has occurred during post animation process

    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.

    Custom Alert Approach

    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

    enter image description here