UIWindowScene instances, take its rootViewController, and iteratively resolve through presentedViewController, UINavigationController.visibleViewController, and UITabBarController.selectedViewController.In UIKit development, deep link handlers, push notification routers, analytics trackers, and coordinator services often need to present an alert or push a new screen from outside a view controller hierarchy. Because these classes lack a direct reference to the current screen, you must programmatically inspect the view controller tree starting from the active window.
Why this happens
Unlike SwiftUI, which drives modal presentations through declarative state bindings, UIKit relies on an explicit hierarchy of UIViewController containers. When you attempt to present a modal using an arbitrary controller that is already presenting another view controller, UIKit throws a runtime warning:
Warning: Attempt to present <UIAlertController: ...> on <MainViewController: ...> which is already presenting <DetailViewController: ...>
Furthermore, legacy methods relying on UIApplication.shared.keyWindow were deprecated in iOS 13 with the introduction of multi-window support via UIWindowScene. Attempting to read keyWindow directly can return nil or point to an inactive window on iPadOS.
Finding the true active screen requires two steps: identifying the currently focused UIWindow from the active scene, and traversing through any active UITabBarController, UINavigationController, or modal presentations (presentedViewController).
How to fix it
1. Create a robust UIViewController traversal extension
A simple recursive or iterative helper function unwraps containers until it reaches the leaf view controller currently on screen.
Add this extension to your project:
import UIKit
extension UIViewController {
/// Returns the topmost view controller currently displayed on screen.
static func topMostViewController() -> UIViewController? {
guard let keyWindow = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.flatMap({ $0.windows })
.first(where: { $0.isKeyWindow }) else {
return nil
}
return topViewController(from: keyWindow.rootViewController)
}
private static func topViewController(from controller: UIViewController?) -> UIViewController? {
// 1. If presented modally, traverse to the presented controller
if let presented = controller?.presentedViewController {
return topViewController(from: presented)
}
// 2. If it is a navigation controller, inspect the visible controller
if let navigation = controller as? UINavigationController {
return topViewController(from: navigation.visibleViewController)
}
// 3. If it is a tab bar controller, inspect the selected tab
if let tab = controller as? UITabBarController {
return topViewController(from: tab.selectedViewController)
}
// 4. Return the terminal view controller
return controller
}
}
You can call this method from anywhere in your codebase:
if let currentVC = UIViewController.topMostViewController() {
let alert = UIAlertController(title: "Session Expired", message: "Please log in again.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
currentVC.present(alert, animated: true)
}
2. Handle nested navigation stacks inside modal sheets
In complex app architectures, modal sheets frequently embed their own UINavigationController or UITabBarController.
By checking controller?.presentedViewController first in the recursive helper (as implemented above), the method dives down into the presentation chain before evaluating whether that modal is itself a navigation stack or tab bar. This guarantees you reach the actual child controller rather than halting at the modal’s container.
If your app presents custom view controller containers (such as side menus or custom drawer controllers), extend the traversal helper to unwrap those specific container types by checking their respective active child controller property.
3. Alternative: Use a dedicated presentation UIWindow
If you frequently display global alerts, interceptor screens, or loading indicators without interrupting whatever navigation stack the user is viewing, navigating the view controller tree can be fragile.
Instead of querying the active controller, create a temporary, high-level UIWindow above the main application window:
final class OverlayManager {
static let shared = OverlayManager()
private var overlayWindow: UIWindow?
func presentGlobalAlert(title: String, message: String) {
guard let scene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }) else {
return
}
let window = UIWindow(windowScene: scene)
window.windowLevel = .alert + 1
window.rootViewController = UIViewController()
window.backgroundColor = .clear
window.makeKeyAndVisible()
self.overlayWindow = window
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
self?.overlayWindow?.isHidden = true
self?.overlayWindow = nil
})
window.rootViewController?.present(alert, animated: true)
}
}
This completely bypasses the need to search for the topmost controller and prevents presentation collision bugs.
How to prevent it
- Adopt coordinator or router patterns: Avoid scattering modal presentation logic across background services, managers, or network callbacks. Delegate routing to dedicated coordinators that track current screen state.
- Avoid
UIApplication.shared.keyWindow: Always queryconnectedScenesto support iOS 13+ and iPadOS multi-scene configurations safely. - Check
isBeingPresentedandisBeingDismissed: Ensure a view controller is stable before attempting another transition on top of it.
FAQ
Why does UIApplication.shared.keyWindow return nil?
Starting in iOS 13, apps support multiple scenes. keyWindow was deprecated because a single global application key window no longer exists; you must query the windows array of the active UIWindowScene.
What happens if a view controller is currently animating during traversal?
If a presentation or dismissal animation is active, presentedViewController or visibleViewController may reflect the intermediate state. In critical navigation flows, wait for completion handlers to finish before initiating another presentation.
Can I use this approach in a SwiftUI application?
While SwiftUI views are hosted inside a UIHostingController, mixing direct UIKit modal presentations with SwiftUI navigation states often breaks SwiftUI environment propagation. Prefer using SwiftUI bindings (.sheet, .fullScreenCover, or .alert) when working in pure SwiftUI apps.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
