Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Cannot show alert over sheet

Tags:

ios

swift

swiftui

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.

like image 361
Stephen.W Avatar asked Jan 18 '26 08:01

Stephen.W


1 Answers

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.

like image 101
EmilioPelaez Avatar answered Jan 20 '26 00:01

EmilioPelaez



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!