Repository: pmusolino/Wormholy
Pull request #149: fix: Fix presenting view controller.
Author: login: akovalov
Labels: name: bug
description: Something isn't working
Created: 2025-02-20T10:33:43Z | Merged: 2025-03-23T15:35:59Z

Fix presenting view controller https://github.com/pmusolino/Wormholy/issues/145 by bringing back `UIViewController.currentViewController()` extension from the Wormholy v1 with minor addition to support `UIHostingController`


Diff:
diff --git a/Sources/WormholySwift/Extensions/UIViewController+Extension.swift b/Sources/WormholySwift/Extensions/UIViewController+Extension.swift
new file mode 100644
index 0000000..ec2f744
--- /dev/null
+++ b/Sources/WormholySwift/Extensions/UIViewController+Extension.swift
@@ -0,0 +1,73 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Alex Kovalov.
+//  Copyright © 2025 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            // In the navigation controller, the visible view controller can be at the top of the navigation stack
+            // or that was presented modally on top of the navigation controller itself
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
+
+        } else if let viewController = viewController as? UITabBarController {
+            // In the tab bar controller, the visible view controller can be selected in the tab bar
+            // or that was presented modally on top of the tab bar controller itself using more tab
+            if let viewControllers = viewController.viewControllers, viewControllers.count > 5, viewController.selectedIndex >= 4 {
+                return currentViewController(viewController.moreNavigationController)
+            } else {
+                return currentViewController(viewController.selectedViewController)
+            }
+
+        } else if let viewController = viewController.presentedViewController {
+            // Presented view controller is the view controller that was presented modally on top of the current view controller
+            // It can be any of the subclass types of UIViewController
+            return currentViewController(viewController)
+
+        } else if viewController.isHosting {
+            // UIKit uses UIHostingController to manage a SwiftUI view hierarchy
+            return viewController
+
+        } else if viewController.children.count > 0 {
+            // When a view controller is a container for other child view controllers
+            return currentViewController(viewController.children.first)
+
+        } else {
+            return viewController
+        }
+    }
+}
+
+extension UIViewController {
+    
+    var isHosting: Bool {
+        let className = NSStringFromClass(type(of: self))
+        if className.contains("UIHostingController") {
+            return true
+        }
+
+        // Check superclass hierarchy to catch subclasses
+        var currentClass: AnyClass? = type(of: self)
+        while let superclass = currentClass?.superclass() {
+            if NSStringFromClass(superclass).contains("UIHostingController") {
+                return true
+            }
+            currentClass = superclass
+        }
+
+        return false
+    }
+}
diff --git a/Sources/WormholySwift/Wormholy.swift b/Sources/WormholySwift/Wormholy.swift
index a361b2a..a36bfd0 100644
--- a/Sources/WormholySwift/Wormholy.swift
+++ b/Sources/WormholySwift/Wormholy.swift
@@ -106,17 +106,17 @@ public class Wormholy: NSObject
     // MARK: - Navigation
     static func presentWormholyFlow() {
         // Check if RequestsView is already presented
-        if let rootViewController = UIApplication.shared.windows.first?.rootViewController,
-           let hostingController = rootViewController.presentedViewController as? UIHostingController<RequestsView> {
+        if let currentViewController = UIViewController.currentViewController(),
+           currentViewController is UIHostingController<RequestsView> {
             // RequestsView is already presented, do nothing
             return
         }
-        
+
         // Present RequestsView as a SwiftUI view
         let requestsView = RequestsView()
         let hostingController = UIHostingController(rootView: requestsView)
         hostingController.modalPresentationStyle = .fullScreen
-        UIApplication.shared.windows.first?.rootViewController?.present(hostingController, animated: true, completion: nil)
+        UIViewController.currentViewController()?.present(hostingController, animated: true, completion: nil)
     }
     
     @objc public static var shakeEnabled: Bool = {


Reviews:
user: login: pmusolino
body: Thank you for fixing this issue, @akovalov!

I just left some suggestions 🙌 
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-02T22:29:19Z
user: login: akovalov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:17:35Z
user: login: akovalov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:22:51Z
user: login: pmusolino
body: It looks great! Thanks for fixing this @akovalov.
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-23T15:35:38Z

Review comments:
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: Since you was the one creating this, you can put your name here.
created_at: 2025-03-02T21:57:47Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: ```suggestion
//  Copyright © 2025 Wormholy. All rights reserved.
```
created_at: 2025-03-02T21:57:55Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: I believe the use of `UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController` can be optimized by using `UIApplication.shared.keyWindow?.rootViewController` which is more efficient. Wdyt?

created_at: 2025-03-02T22:01:13Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
+        } else if let viewController = viewController as? UITabBarController {
+            if let viewControllers = viewController.viewControllers, viewControllers.count > 5, viewController.selectedIndex >= 4 {
+                return currentViewController(viewController.moreNavigationController)
+            } else {
+                return currentViewController(viewController.selectedViewController)
+            }
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: It would be helpful to clarify this logic and determine if it makes sense to add a comment for future reference.
created_at: 2025-03-02T22:03:07Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
+        } else if let viewController = viewController as? UITabBarController {
+            if let viewControllers = viewController.viewControllers, viewControllers.count > 5, viewController.selectedIndex >= 4 {
+                return currentViewController(viewController.moreNavigationController)
+            } else {
+                return currentViewController(viewController.selectedViewController)
+            }
+        } else if let viewController = viewController.presentedViewController {
+            return currentViewController(viewController)
+        } else if viewController.isHosting {
+            return viewController
+        } else if viewController.children.count > 0 {
+            return viewController.children[0]
+        } else {
+            return viewController
+        }
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: Also here, I would add a comment here that clarifies every condition as a future reminder.
created_at: 2025-03-02T22:04:22Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: In general, I would add a comment here that clarifies every condition as a future reminder.
created_at: 2025-03-02T22:04:59Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
+        } else if let viewController = viewController as? UITabBarController {
+            if let viewControllers = viewController.viewControllers, viewControllers.count > 5, viewController.selectedIndex >= 4 {
+                return currentViewController(viewController.moreNavigationController)
+            } else {
+                return currentViewController(viewController.selectedViewController)
+            }
+        } else if let viewController = viewController.presentedViewController {
+            return currentViewController(viewController)
+        } else if viewController.isHosting {
+            return viewController
+        } else if viewController.children.count > 0 {
+            return viewController.children[0]
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: I think returning the first child view controller without further checks might lead to unexpected behavior if the first child is not the intended view controller. So consider something like this:
```
return currentViewController(viewController.children.first)
```
created_at: 2025-03-02T22:08:20Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
+        } else if let viewController = viewController as? UITabBarController {
+            if let viewControllers = viewController.viewControllers, viewControllers.count > 5, viewController.selectedIndex >= 4 {
+                return currentViewController(viewController.moreNavigationController)
+            } else {
+                return currentViewController(viewController.selectedViewController)
+            }
+        } else if let viewController = viewController.presentedViewController {
+            return currentViewController(viewController)
+        } else if viewController.isHosting {
+            return viewController
+        } else if viewController.children.count > 0 {
+            return viewController.children[0]
+        } else {
+            return viewController
+        }
+    }
+}
+
+extension UIViewController {
+
+    var isHosting: Bool {
+        String(describing: self).contains("UIHostingController")
+    }
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: This implementation is fragile and not type safe.
I think a little bit more robust solution would be 

```
    var isHosting: Bool {
        NSStringFromClass(type(of: self)).contains("UIHostingController")
    }
```

In this case, it uses `type(of: self)` instead of referring to the instance itself. Additionally, `NSStringFromClass` is more reliable for obtaining the actual class name. This approach also addresses the potential issue with generic types, as `UIHostingController<Content>` cannot be directly checked using `is`. Also, this solution is faster in terms of performance.

But... if someone creates a custom class that inherits from UIHostingController, what happens?
Eg.
```
class XYZHostingController<Content: View>: UIHostingController<Content> { ... }
```

The string description would be "XYZHostingController" which doesn't contain "UIHostingController", causing the check to fail.
So, I think more reliable approach would use the type hierarchy:
```suggestion
var isHosting: Bool {
    let className = NSStringFromClass(type(of: self))
    if className.contains("UIHostingController") {
        return true
    }
    
    // Check superclass hierarchy to catch subclasses
    var currentClass: AnyClass? = type(of: self)
    while let superclass = currentClass?.superclass() {
        if NSStringFromClass(superclass).contains("UIHostingController") {
            return true
        }
        currentClass = superclass
    }
    
    return false
}
```

created_at: 2025-03-02T22:24:56Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: pmusolino
body: I wonder if it's better to rename this file to `UIViewController+Utils` or `UIViewController+Extension` to be more clear.
created_at: 2025-03-02T22:28:31Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: akovalov
body: @pmusolino [UIApplication.keyWindow](https://developer.apple.com/documentation/uikit/uiapplication/keywindow) is deprecated since iOS 13. It will give a compilation warning even though it continues working for now, so looks like it will not be much better. 
created_at: 2025-03-04T10:17:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+//
+//  Wormholy.swift
+//  Wormholy
+//
+//  Created by Paolo Musolino.
+//  Copyright © 2018 Wormholy. All rights reserved.
+//
+
+import UIKit
+import SwiftUI
+
+extension UIViewController {
+
+    static func currentViewController(_ viewController: UIViewController? = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController) -> UIViewController? {
+        guard let viewController else { return nil }
+
+        if let viewController = viewController as? UINavigationController {
+            if let viewController = viewController.visibleViewController {
+                return currentViewController(viewController)
+            } else {
+                return currentViewController(viewController.topViewController)
+            }
+        } else if let viewController = viewController as? UITabBarController {
+            if let viewControllers = viewController.viewControllers, viewControllers.count > 5, viewController.selectedIndex >= 4 {
+                return currentViewController(viewController.moreNavigationController)
+            } else {
+                return currentViewController(viewController.selectedViewController)
+            }
+        } else if let viewController = viewController.presentedViewController {
+            return currentViewController(viewController)
+        } else if viewController.isHosting {
+            return viewController
+        } else if viewController.children.count > 0 {
+            return viewController.children[0]
+        } else {
+            return viewController
+        }
+    }
+}
+
+extension UIViewController {
+
+    var isHosting: Bool {
+        String(describing: self).contains("UIHostingController")
+    }
path: Sources/WormholySwift/Extensions/UIViewController+Ext.swift
user: login: akovalov
body: @pmusolino it's a great suggestion! Thanks
created_at: 2025-03-04T10:22:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: akovalov
created_at: 2025-03-04T10:45:51Z
body: Thank you @pmusolino Please re-review and correct it if needed. 
author_association: CONTRIBUTOR
user: login: Silviuvr
created_at: 2025-03-19T13:57:03Z
body: Would really come in handy to merge this.
author_association: NONE
user: login: pmusolino
created_at: 2025-03-19T16:32:51Z
body: Sorry for the late response, I'll review it this weekend. 👍 
author_association: OWNER
user: login: pmusolino
created_at: 2025-03-23T15:42:50Z
body: This is now part of release 2.1.0: https://github.com/pmusolino/Wormholy/releases/tag/2.1.0
author_association: OWNER