Does swiftUI .alert miss a completion handler?

Hi, I am currently running into a strange issue. I am talking to an external device which can send multiple sequential warnings.

I am using the new view.alert(title, isPresented, actions, message) modifier.

Sometimes it happens that the external message becomes irrelevant in which case I clear the isPresented Binding to remove the Alert and present the next one if there is one.

I use 2 ObjervableObjects - one that contains the isPresented flag and one for the content of the Message. If I reset isPresented, update the content ObservableObject and set isPresented again, the Alert window cannot get dismissid by setting isPresented to false again. The Alert just stays along and the new Alert does not get presented. (although somehow the UI believes it is presenting a new Alert)

I found that I need to wait a bit after clearing iPresented and do an DispatchQueue.main.asyncAfter to set my iPresented flag - to allow the Alert window to vanish and the views to update.

If there is a timing problem with dismissing the .alert - shouldn't there be a completion handler for the alert - obviously putting in a random delay seems hacky...?

Best, Michael

Here is a little example of what I mean. If you take out the delay - it stops working:

import Combine

class Messages : ObservableObject {
    static var shared = Messages()

    @Published var isPresented:Bool = false
    @Published var message:String = ""

    var messages = ["One", "Two", "Three", "Four", "Five" ]
    var subscriptions:[AnyCancellable] = []

    init() {
        $isPresented
            .delay(for: .seconds(1), scheduler: DispatchQueue.main) // <- Take out this delay and it stops working.
            .receive(on: DispatchQueue.main)
            .sink{ [weak self] state in
                print ("Getting new state: \(state)")
                if state == false {
                    self?.next()

                    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
                        if self?.isPresented == true {
                            self?.isPresented = false
                        }
                    }
                }
            }
            .store(in: &subscriptions)
    }

    func next() {
        if  messages.isEmpty == false {
            self.message = messages.removeFirst()
            self.isPresented = true
            print ("Next Message \(self.message)")
        }
    }
}




@main
struct DummyApp: App {

    @ObservedObject var messages = Messages.shared
    @State var isPresented = false

    var body: some Scene {
        WindowGroup {

            VStack {
                Button("isPresented: \(String(describing:messages.isPresented))"){
                    messages.isPresented = true
                }
                .frame(width:150, height:100)


                .alert("My Alert", isPresented: $isPresented, actions: { Button("OK"){} }, message:{ Text(messages.message) } )
                .onReceive(messages.$isPresented){ state in
                    isPresented = state
                }

                Color.clear
                    .frame(width: 100,height: 300)
            }

        }
    }
}

And here ist the Output I ge when the delay is remove...

You could actually argue that .alert is not respecting SwiftUIs core principles of "One Truth" ;-)

Next Message One
Getting new state: true
Getting new state: false
Next Message Two
Getting new state: true
Getting new state: false
Next Message Three
2022-12-05 09:15:21.280381+0100 Dummy[15473:8198016] [Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x13f052000> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x159814a00> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x159814a00>) which is already presenting <SwiftUI.PlatformAlertController: 0x158037800>.
Getting new state: true
Getting new state: false
Next Message Four
2022-12-05 09:15:24.429913+0100 Dummy[15473:8198016] [Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x13f052000> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x159814a00> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x159814a00>) which is already presenting <SwiftUI.PlatformAlertController: 0x158037800>.
Getting new state: true
Getting new state: false
Next Message Five
2022-12-05 09:15:27.584491+0100 Dummy[15473:8198016] [Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x13f052000> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x159814a00> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x159814a00>) which is already presenting <SwiftUI.PlatformAlertController: 0x158037800>.
Getting new state: true
Getting new state: false

In addition - this behavior is quite nasty if the alert is presented ontop of a sheet or popover. If the sheet is dismissed before the alert has completed its transition the sheet is stuck.

What happens if you replay @ObservedObject with @StateObject in your App struct?

In my app the object collecting errors and alerts (also from an external device) is a global singleton and cannot live only in the UI. Also this is very likely a problem with the UiKit viewcontroller that provides the Alert and its transition. The reference to the observed object is never lost - otherwise it wouldn't work with the delay. The real problem from my perspective is that the transition of the Alert that is not finished.

Does swiftUI .alert miss a completion handler?
 
 
Q