From ba9d72e5d019514e04c0fbb9f15baaeeba5f670b Mon Sep 17 00:00:00 2001 From: Matt Langston Date: Tue, 20 Oct 2026 11:07:08 -0500 Subject: [PATCH] fix(dialog): present dialogs from the topmost view controller on iOS Present each alert from the topmost presented view controller instead of the bridge view controller, so a dialog appears above any view controller the app has presented over the web view. UIKit refuses to present from a view controller that already presents one, and the refusal has no error path, so the call never resolved. If the walk stops at a controller whose presented child is still being dismissed, reject the call instead of leaving it pending. Closes #2404 --- .../Sources/DialogPlugin/DialogPlugin.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/dialog/ios/Sources/DialogPlugin/DialogPlugin.swift b/dialog/ios/Sources/DialogPlugin/DialogPlugin.swift index 4fdb1743b2..73c9f2b81e 100644 --- a/dialog/ios/Sources/DialogPlugin/DialogPlugin.swift +++ b/dialog/ios/Sources/DialogPlugin/DialogPlugin.swift @@ -27,7 +27,7 @@ public class DialogPlugin: CAPPlugin, CAPBridgedPlugin { alert.addAction(UIAlertAction(title: buttonTitle, style: UIAlertAction.Style.default, handler: { (_) in call.resolve() })) - self?.bridge?.viewController?.present(alert, animated: true, completion: nil) + self?.present(alert, for: call) } } @@ -52,7 +52,7 @@ public class DialogPlugin: CAPPlugin, CAPBridgedPlugin { "value": true ]) })) - self?.bridge?.viewController?.present(alert, animated: true, completion: nil) + self?.present(alert, for: call) } } @@ -89,7 +89,22 @@ public class DialogPlugin: CAPPlugin, CAPBridgedPlugin { ]) })) - self?.bridge?.viewController?.present(alert, animated: true, completion: nil) + self?.present(alert, for: call) } } + + // UIKit refuses to present from a view controller that already presents one, so climb to + // the topmost presented controller. A controller that is mid-dismissal cannot present, and + // neither can its presenter until the dismissal ends, so reject in that window. + private func present(_ alert: UIAlertController, for call: CAPPluginCall) { + var presenter = bridge?.viewController + while let presented = presenter?.presentedViewController, !presented.isBeingDismissed { + presenter = presented + } + guard let presenter, presenter.presentedViewController == nil else { + call.reject("Can't present a dialog while another view controller is being dismissed") + return + } + presenter.present(alert, animated: true, completion: nil) + } }