Rehan Ali Khan
Rehan Ali Khan

Reputation: 601

How to Get the Top-Most View Controller in iPad with Multiple Scenes (UIScene) in Swift?

I have an existing method to get the top-most view controller in iOS applications without multiple scenes, which works perfectly for single window applications on both iPhone and iPad:

extension UIApplication {

    // Get top most view controller in App.

    class func getTopMostViewController(base: UIViewController? = UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController) -> UIViewController? {
        
        if let nav = base as? UINavigationController {
            return getTopMostViewController(base: nav.visibleViewController)
        }
        
        if let tab = base as? UITabBarController {
            if let selected = tab.selectedViewController {
                return getTopMostViewController(base: selected)
            }
        }
        if let presented = base?.presentedViewController {
            return getTopMostViewController(base: presented)
        }
        
        return base
    }
}

However, with the introduction of multiple scenes (UIScene) in iOS 13, I need a way to get the top-most view controller that can handle multiple windows and scenes. I adapted the function as follows:

extension UIApplication {
    
    class func getTopViewController(base: UIViewController? = keyWindow?.rootViewController) -> UIViewController? {
        // Find the most visible view controller in the hierarchy
        
        if let nav = base as? UINavigationController {
            return getTopViewController(base: nav.visibleViewController)
        }
        
        if let tab = base as? UITabBarController {
            if let selected = tab.selectedViewController {
                return getTopViewController(base: selected)
            }
        }
        
        if let presented = base?.presentedViewController {
            return getTopViewController(base: presented)
        }
        
        return base
    }

    private static var keyWindow: UIWindow? {
        // Get connected scenes
        return UIApplication.shared.connectedScenes
            // Keep only active scenes, onscreen and visible to the user
            .filter { $0.activationState == .foregroundActive }
            // Keep only the first `UIWindowScene`
            .compactMap { $0 as? UIWindowScene }
            // Get its associated windows
            .flatMap { $0.windows }
            // Finally, keep only the key window
            .first { $0.isKeyWindow }
    }
}

This works fine most of the time, but occasionally, keyWindow is nil on iPhone. I tried adding a 0.5 seconds delay and making the function asynchronous, which worked, but I want a synchronous solution that reliably works for both single and multiple scenes/windows.

How can I modify this function to always get the correct top-most view controller in a synchronous manner?

Additional Context:

I am targeting iOS 13 and above.

Note: First and foremost i want the second function to work on iPhone in every case currently when camera permission dialog closes and i immediately call the second function on dialog's callback, then this issue occurs, where keywindow is nil.

Upvotes: 0

Views: 79

Answers (0)

Related Questions