From 9400e2b445bf1a3a9f9a7d3b2c4e84113c070a29 Mon Sep 17 00:00:00 2001 From: narjes Date: Wed, 6 Aug 2025 10:33:19 +0200 Subject: [PATCH 1/6] IOS-6487 Add topMostVC for presenter --- .../UIWindow+topMostViewController.swift | 26 +++++++++++++++++++ .../Manager/PresentSurveyManager.swift | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift diff --git a/Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift b/Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift new file mode 100644 index 00000000..d72e44c9 --- /dev/null +++ b/Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift @@ -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 + } + } + } +} diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index 3cd731f9..c8bc908a 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -25,7 +25,7 @@ final class PresentSurveyManager { presentationController.detents = [.large()] } self.viewController = vc - window.rootViewController?.present(vc, animated: true, completion: nil) + window.topMostViewController()?.present(vc, animated: true, completion: nil) } } } From ceab083672b7d7773d6a628b811ac0e83fd52fba Mon Sep 17 00:00:00 2001 From: Narjes Date: Mon, 11 Aug 2025 16:40:15 +0200 Subject: [PATCH 2/6] Update bg color --- Sources/FormbricksSDK/Manager/PresentSurveyManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index c8bc908a..4cbc30c0 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -20,7 +20,7 @@ final class PresentSurveyManager { let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id)) let vc = UIHostingController(rootView: view) vc.modalPresentationStyle = .overCurrentContext - vc.view.backgroundColor = UIColor.gray.withAlphaComponent(0.6) + vc.view.backgroundColor = UIColor(red: 0x1E/255.0, green: 0x1E/255.0, blue: 0x1E/255.0, alpha: 0.2) if let presentationController = vc.presentationController as? UISheetPresentationController { presentationController.detents = [.large()] } From 70ddb6ff8a6c34e400552f137efa3abb58b28d68 Mon Sep 17 00:00:00 2001 From: Narjes Date: Mon, 11 Aug 2025 16:41:01 +0200 Subject: [PATCH 3/6] Update FormbricksView fix gap --- Sources/FormbricksSDK/WebView/FormbricksView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/FormbricksSDK/WebView/FormbricksView.swift b/Sources/FormbricksSDK/WebView/FormbricksView.swift index c1ee78c5..e5c482e0 100644 --- a/Sources/FormbricksSDK/WebView/FormbricksView.swift +++ b/Sources/FormbricksSDK/WebView/FormbricksView.swift @@ -7,6 +7,7 @@ struct FormbricksView: View { var body: some View { if let htmlString = viewModel.htmlString { SurveyWebView(surveyId: viewModel.surveyId, htmlString: htmlString) + .ignoresSafeArea() } } } From b7a6bebb378364965f468b419535433cb2180841 Mon Sep 17 00:00:00 2001 From: "Hassaan F. Ahmed" Date: Wed, 18 Feb 2026 08:53:42 +0400 Subject: [PATCH 4/6] Issue fixed where the survey would not be presented when there is a viewcontroller already presented --- FormbricksSDK.podspec | 2 +- .../Manager/PresentSurveyManager.swift | 58 ++++++++++++++----- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/FormbricksSDK.podspec b/FormbricksSDK.podspec index e66f8e22..362fb522 100644 --- a/FormbricksSDK.podspec +++ b/FormbricksSDK.podspec @@ -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" } diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index 5875cf84..7b8f5f11 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -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) + } + 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) + }) } } From fdefe7edc155e65f8013ceb332aebfab1a94ddb8 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Fri, 8 May 2026 12:11:04 +0530 Subject: [PATCH 5/6] fixes --- .../UIWindow+topMostViewController.swift | 26 --------- .../Helpers/FormbricksEnvironment.swift | 3 +- .../Manager/PresentSurveyManager.swift | 54 +++++++++---------- 3 files changed, 28 insertions(+), 55 deletions(-) delete mode 100644 Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift diff --git a/Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift b/Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift deleted file mode 100644 index d72e44c9..00000000 --- a/Sources/FormbricksSDK/Extension/UIWindow+topMostViewController.swift +++ /dev/null @@ -1,26 +0,0 @@ -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 - } - } - } -} diff --git a/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift b/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift index 31111174..dc2974a3 100644 --- a/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift +++ b/Sources/FormbricksSDK/Helpers/FormbricksEnvironment.swift @@ -20,7 +20,8 @@ internal enum FormbricksEnvironment { /// Returns the full environment‐fetch URL as a String for the given ID static var getEnvironmentRequestEndpoint: String { - return ["api", "v2", "client", "{environmentId}", "environment"].joined(separator: "/") + let path = ["api", "v2", "client", "{environmentId}", "environment"].joined(separator: "/") + return path } /// Returns the full post-user URL as a String for the given ID diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index 7b8f5f11..1231292c 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -1,6 +1,6 @@ import SwiftUI -/// Presents a survey webview to the window's root +/// Presents a survey webview from the top-most view controller in the key window. final class PresentSurveyManager { init() { /* @@ -8,58 +8,56 @@ final class PresentSurveyManager { The class serves as a namespace for the present method, so instance creation is not needed and should be restricted. */ } - + /// 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 { + /// Walks the active presentation/navigation/tab hierarchy and returns the leaf VC. + /// Mirrors UIKit's own `presentedViewController` traversal so a single walker is enough. + private func topMostViewController(from viewController: UIViewController) -> UIViewController { if let presented = viewController.presentedViewController { - return topViewController(from: presented) + return topMostViewController(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 topMostViewController(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 topMostViewController(from: selected) } 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. + /// Present the webview as a page sheet over the current top-most view controller. func present(environmentResponse: EnvironmentResponse, id: String, completion: ((Bool) -> Void)? = nil) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } + guard let window = UIApplication.safeKeyWindow, let rootVC = window.rootViewController else { + Formbricks.logger?.error("Survey present aborted: no key window or root view controller available.") 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) - } + let presenter = self.topMostViewController(from: rootVC) - // Check if presenter is already presenting - guard presenter.presentedViewController == nil else { + // UIAlertController/action-sheets/popovers cannot host a modal sheet — presenting on them either + // crops the survey to the alert frame or is rejected by UIKit. Bail with a clear log so the host + // app can dismiss the alert before triggering the survey. + if presenter is UIAlertController { + Formbricks.logger?.warning("Survey present aborted: top-most VC is a UIAlertController. Dismiss it before triggering the survey.") completion?(false) return } let view = FormbricksView(viewModel: FormbricksViewModel(environmentResponse: environmentResponse, surveyId: id)) let vc = UIHostingController(rootView: view) - vc.modalPresentationStyle = .overCurrentContext + vc.modalPresentationStyle = .pageSheet vc.view.backgroundColor = .clear - if let presentationController = vc.presentationController as? UISheetPresentationController { - presentationController.detents = [.large()] + if let sheet = vc.sheetPresentationController { + sheet.detents = [.large()] } self.viewController = vc presenter.present(vc, animated: true, completion: { @@ -67,12 +65,12 @@ final class PresentSurveyManager { }) } } - + /// Dismiss the webview func dismissView() { viewController?.dismiss(animated: true) } - + deinit { Formbricks.logger?.debug("Deinitializing \(self)") } From 5b2599f04b8b4a760c2b1c79041fd618090e3f97 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Fri, 8 May 2026 12:23:05 +0530 Subject: [PATCH 6/6] fixes coderabbit feedback --- Sources/FormbricksSDK/Manager/PresentSurveyManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift index 1231292c..fc22e72d 100644 --- a/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift +++ b/Sources/FormbricksSDK/Manager/PresentSurveyManager.swift @@ -15,7 +15,8 @@ final class PresentSurveyManager { /// Walks the active presentation/navigation/tab hierarchy and returns the leaf VC. /// Mirrors UIKit's own `presentedViewController` traversal so a single walker is enough. private func topMostViewController(from viewController: UIViewController) -> UIViewController { - if let presented = viewController.presentedViewController { + if let presented = viewController.presentedViewController, + !presented.isBeingDismissed { return topMostViewController(from: presented) } if let navigation = viewController as? UINavigationController,