BS
BleepingSwift
Published on

> Finding the Top View Controller in Swift

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

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.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.