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
If you really need that, I think there is only a bit of hacky solution for that:
The other solution that comes to my mind is that you can watch
.onChange
forisPresentingSheet
and call the alert from there (I haven’t tested this one it might end up to needasyncAfter
too). Obviously you need to add some more variables to know what has happened in yoursheet
as every dismiss shouldn’t call the alert. It is less hacky but more complex to implement and less readable overall.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
toUINavigationController
, andTabView
toUITabViewController
.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.