- Published on
- 4 min read
> Finding the Top View Controller in Swift
Sometimes you need to present a view controller from code that doesn't have direct access to the current view hierarchy. A networking layer showing an auth prompt, an analytics handler displaying an alert, or a deep link handler pushing a new screen. Finding the "top" view controller requires traversing through presented controllers, navigation stacks, and tab bars to locate the one currently visible to the user.
The Traversal Logic
The top view controller isn't simply the root view controller of your window. It's whatever controller is currently on screen, which might be several layers deep: a tab bar containing a navigation controller that presented a modal which pushed another view controller. You need to walk this chain to find the end.
extension UIWindow {
@MainActor
func topViewController() -> UIViewController? {
var current = rootViewController
while let next = current?.presentedViewController ??
(current as? UINavigationController)?.visibleViewController ??
(current as? UITabBarController)?.selectedViewController {
current = next
}
return current
}
}
This walks the hierarchy by checking three possibilities at each step: if there's a presented modal, follow it; if the current controller is a navigation controller, get its visible controller; if it's a tab bar, get the selected tab. When none of these apply, you've found the top.
Getting the Window
To use this, you need a reference to the window. In modern iOS with scenes:
@MainActor
func getTopViewController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }),
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else {
return nil
}
return window.topViewController()
}
Or as a convenience extension on UIApplication:
extension UIApplication {
@MainActor
var topViewController: UIViewController? {
guard let windowScene = connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }),
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else {
return nil
}
return window.topViewController()
}
}
// Usage
if let top = UIApplication.shared.topViewController {
top.present(alertController, animated: true)
}
A More Explicit Implementation
If you prefer clarity over conciseness, here's the same logic written out:
extension UIWindow {
@MainActor
func topViewController() -> UIViewController? {
var top = rootViewController
while true {
if let presented = top?.presentedViewController {
top = presented
} else if let nav = top as? UINavigationController {
top = nav.visibleViewController
} else if let tab = top as? UITabBarController {
top = tab.selectedViewController
} else {
break
}
}
return top
}
}
This version makes the priority order explicit: presented view controllers take precedence, then navigation controller contents, then tab bar selection.
Handling Custom Container Controllers
If your app uses custom container view controllers, the standard traversal won't know how to find their visible child. Extend the logic to handle your containers:
extension UIWindow {
@MainActor
func topViewController() -> UIViewController? {
var current = rootViewController
while true {
if let presented = current?.presentedViewController {
current = presented
} else if let nav = current as? UINavigationController {
current = nav.visibleViewController
} else if let tab = current as? UITabBarController {
current = tab.selectedViewController
} else if let splitView = current as? UISplitViewController {
current = splitView.viewControllers.last
} else if let pageView = current as? UIPageViewController {
current = pageView.viewControllers?.first
} else {
break
}
}
return current
}
}
Add cases for any custom container types your app defines.
When to Use This
Finding the top view controller is useful for presenting alerts, handling deep links, or responding to push notifications when you're in code that doesn't naturally have view controller context. However, use this sparingly. If you find yourself reaching for the top view controller frequently, consider whether your architecture could be improved, perhaps with a coordinator pattern or a dedicated presentation service that already has the right context.
The @MainActor annotation ensures this code runs on the main thread, which is required since UIApplication and view controllers must only be accessed from the main thread.
// Continue_Learning
The Difference Between Frame and Bounds in UIKit
Understand the key difference between a UIView's frame and bounds properties, and when to use each in your iOS apps.
What's New in StoreKit for Xcode 27: Commitment Plans, Offer Codes, and More
Xcode 27 brings monthly subscriptions with a 12-month commitment, a reworked offer code redemption API, Bundles and Suites, and a unified App Review submission flow. Here is what each one changes for your code.
App Tracking Transparency in SwiftUI
A practical guide to App Tracking Transparency in iOS, covering the Info.plist usage description, the ATTrackingManager API, authorization statuses, and when to actually prompt the user from SwiftUI.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.