skip to Main Content

The story is a little long. First, let me show some code that doesn’t work as I expected.

import SwiftUI

struct ContentView: View {
    @State var isPresentingAlert = false
    @State var isPresentingSheet = false
    
    var body: some View {
        VStack {
            Button("Show Sheet") {
                isPresentingSheet = true
            }
        }
        .alert("Alert", isPresented: $isPresentingAlert) {}
        .sheet(isPresented: $isPresentingSheet) {
            Button("Show Alert") {
                isPresentingAlert = true
            }
        }
    }
}

The code’s intent is clear: the view should display a sheet when user press the "Show Sheet" button, and there is a "Show Alert" button on the sheet, which should cause the alert window to show after tapping. The code is simplified – in the original app, the sheet shows a form to the user, and the alert should show when user submit a form with some invalid data.

But actually when I pressed the "Show Alert" button, the alert wasn’t shown. Instead, I got following error in the console:

2023-02-14 23:41:08.163349+0800 SheetAndAlert[8351:217492] [Presentation] Attempt to
present <SwiftUI.PlatformAlertController: 0x15b030600> on
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x15b00aa00> (from
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x15b00aa00>) which is already presenting
<_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x15b02e000>.

I had no idea what does this error means. Later I changed the code to use UIKIt’s UIAlertController, wishing I can get around the error. (The code was adapted from this answer: How do I present a UIAlertController in SwiftUI?)

import SwiftUI 

struct ContentView: View {
    @State var isPresentingAlert = false
    @State var isPresentingSheet = false
    
    var body: some View {
        VStack {
            Button("Show Sheet") {
                isPresentingSheet = true
            }
        }
        .sheet(isPresented: $isPresentingSheet) {
            Button("Show Alert") {
                ContentView.alertMessage(title: "Alert", message: "Alert")
            }
        }
    }
    
    static func alertMessage(title: String, message: String) {
        let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default) { (action: UIAlertAction) in
        }
        alertVC.addAction(okAction)
        
        let viewController = UIApplication.shared.windows.first!.rootViewController!
        viewController.present(alertVC, animated: true, completion: nil)
    }
}

But still, it doesn’t work. A similiar error is thrown:

2023-02-14 23:56:12.715039+0800 SheetAndAlert[9991:263667] [Presentation] Attempt to
present <UIAlertController: 0x13d80b800> on
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x139012600> (from
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x139012600>) which is already presenting
<_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x13b02fc00>.

Finally I found a working solution, just move .alert inside .sheet‘s content:

import SwiftUI

struct ContentView: View {
    @State var isPresentingAlert = false
    @State var isPresentingSheet = false
    
    var body: some View {
        VStack {
            Button("Show Sheet") {
                isPresentingSheet = true
            }
        }
        .sheet(isPresented: $isPresentingSheet) {
            Button("Show Alert") {
                isPresentingAlert = true
            }
            .alert("Alert", isPresented: $isPresentingAlert) {}
        }
    }
}

Though works, but I still don’t understand (at all!) why using UIAlertController cannot solve the problem, while just moving the modifier to another place can.

2

Answers


  1. If you really need that, I think there is only a bit of hacky solution for that:

    .sheet(isPresented: $isPresentingSheet) {
        Button("Show Alert") {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                 isPresentingAlert = true
            }
        }
    }
    

    The other solution that comes to my mind is that you can watch .onChange for isPresentingSheet and call the alert from there (I haven’t tested this one it might end up to need asyncAfter too). Obviously you need to add some more variables to know what has happened in your sheet as every dismiss shouldn’t call the alert. It is less hacky but more complex to implement and less readable overall.

    Login or Signup to reply.
  2. TL;DR: This behavior is a constraint of the UIKit layer used by SwiftUI and the solution is what you already found, to present the alert from the body of the .sheet modifier.


    The behavior you’re seeing is caused by the underlying UIKit layer that SwiftUI relies on.

    In UIKit, content is managed by UIViewControllers, each one of these view controllers can present another view controller on top of it, but only one. This is often used to present "modals" (or sheets), "popovers" and "alerts".

    View controllers are invisible to us when using SwiftUI, but they are still there. Some SwiftUI structs map to some UIKit view controllers, like NavigationView to UINavigationController, and TabView to UITabViewController.

    As you may have guessed by now, the .sheet and the .alert modifiers map to their own view controllers. .sheet specifically creates a new view controller that is presented on top of the current view controller.

    As I mentioned earlier, view controllers in UIKit can present only one view controller at any time. Because both modifiers are modifying the same content, and hence the same view controller, trying to activate both at the same time will give you that error.

    The fix is what you already found: moving the .alert modifier inside the body of .sheet will cause the alert to be presented on the sheet view controller.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search