Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FormbricksSDK.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "FormbricksSDK"
s.version = "1.1.0"
s.version = "1.2.0"
s.summary = "iOS SDK for Formbricks"
s.homepage = "https://github.com/formbricks/ios"
s.license = { :type => "MIT", :file => "LICENSE" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import UIKit

extension UIWindow {
func topMostViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.topMostViewControllerFrom(rootViewController)
}
return nil
}

static func topMostViewControllerFrom(_ viewController: UIViewController) -> UIViewController {
if let navigationController = viewController as? UINavigationController,
let visibleController = navigationController.visibleViewController {
return topMostViewControllerFrom(visibleController)
} else if let tabBarController = viewController as? UITabBarController,
let selectedTabController = tabBarController.selectedViewController {
return topMostViewControllerFrom(selectedTabController)
} else {
if let presentedViewController = viewController.presentedViewController {
return topMostViewControllerFrom(presentedViewController)
} else {
return viewController
}
}
}
}
58 changes: 45 additions & 13 deletions Sources/FormbricksSDK/Manager/PresentSurveyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,60 @@ final class PresentSurveyManager {

/// The view controller that will present the survey window.
private weak var viewController: UIViewController?


/// Finds the topmost view controller in the hierarchy to present from
private func topViewController(from viewController: UIViewController) -> UIViewController {
if let presented = viewController.presentedViewController {
return topViewController(from: presented)
}
if let navigation = viewController as? UINavigationController {
return topViewController(from: navigation.visibleViewController ?? navigation)
}
if let tabBar = viewController as? UITabBarController {
return topViewController(from: tabBar.selectedViewController ?? tabBar)
}
Comment on lines +20 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Infinite recursion when visibleViewController or selectedViewController is nil.

If a UINavigationController has no view controllers in its stack (visibleViewController == nil), or a UITabBarController has no tabs (selectedViewController == nil), the fallback ?? navigation / ?? tabBar passes the same container back to topViewController, causing infinite recursion and a stack overflow.

🐛 Proposed fix to prevent infinite recursion
     private func topViewController(from viewController: UIViewController) -> UIViewController {
         if let presented = viewController.presentedViewController {
             return topViewController(from: presented)
         }
-        if let navigation = viewController as? UINavigationController {
-            return topViewController(from: navigation.visibleViewController ?? navigation)
+        if let navigation = viewController as? UINavigationController,
+           let visible = navigation.visibleViewController {
+            return topViewController(from: visible)
         }
-        if let tabBar = viewController as? UITabBarController {
-            return topViewController(from: tabBar.selectedViewController ?? tabBar)
+        if let tabBar = viewController as? UITabBarController,
+           let selected = tabBar.selectedViewController {
+            return topViewController(from: selected)
         }
         return viewController
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let navigation = viewController as? UINavigationController {
return topViewController(from: navigation.visibleViewController ?? navigation)
}
if let tabBar = viewController as? UITabBarController {
return topViewController(from: tabBar.selectedViewController ?? tabBar)
}
private func topViewController(from viewController: UIViewController) -> UIViewController {
if let presented = viewController.presentedViewController {
return topViewController(from: presented)
}
if let navigation = viewController as? UINavigationController,
let visible = navigation.visibleViewController {
return topViewController(from: visible)
}
if let tabBar = viewController as? UITabBarController,
let selected = tabBar.selectedViewController {
return topViewController(from: selected)
}
return viewController
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/FormbricksSDK/Manager/PresentSurveyManager.swift` around lines 20 -
25, The helper topViewController(from:) can recurse infinitely when a
UINavigationController has visibleViewController == nil or a UITabBarController
has selectedViewController == nil because the current code falls back to passing
the container (navigation/tabBar) back into topViewController; fix by checking
for the child first (use guard/if let) and if nil return the container itself
(or its top-most child if appropriate) instead of calling topViewController with
the same container; update the branches that reference
UINavigationController.visibleViewController and
UITabBarController.selectedViewController to unwrap into a local
childViewController and only recurse when that child is non-nil.

return viewController
}

/// Present the webview
/// The native background is always `.clear` — overlay rendering is handled
/// entirely by the JS survey library inside the WebView to avoid double-overlay artifacts.
func present(environmentResponse: EnvironmentResponse, id: String, completion: ((Bool) -> Void)? = nil) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let window = UIApplication.safeKeyWindow {
let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id))
let vc = UIHostingController(rootView: view)
vc.modalPresentationStyle = .overCurrentContext
vc.view.backgroundColor = .clear
if let presentationController = vc.presentationController as? UISheetPresentationController {
presentationController.detents = [.large()]
}
self.viewController = vc
window.rootViewController?.present(vc, animated: true, completion: {
completion?(true)
})
guard let window = UIApplication.safeKeyWindow,
let rootVC = window.rootViewController else {
completion?(false)
return
}

// Determine the presenter: use root if available, otherwise find topmost
let presenter: UIViewController
if rootVC.presentedViewController == nil {
// Root is free, use it directly (simple path)
presenter = rootVC
} else {
// Root is already presenting, find the topmost view controller
presenter = self.topViewController(from: rootVC)
}

// Check if presenter is already presenting
guard presenter.presentedViewController == nil else {
completion?(false)
return
}

let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id))
let vc = UIHostingController(rootView: view)
vc.modalPresentationStyle = .overCurrentContext
vc.view.backgroundColor = .clear
if let presentationController = vc.presentationController as? UISheetPresentationController {
presentationController.detents = [.large()]
}
self.viewController = vc
presenter.present(vc, animated: true, completion: {
completion?(true)
})
}
}

Expand Down