From f7703c466f03edfd3dea804833fc64fef6d83b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:38:45 +0200 Subject: [PATCH 1/2] Don't hold the delegate when UIKit refuses to show the Customer Center UIKit refuses some presentations, such as from a view controller that isn't in a window, and only logs it. The manager stored the delegate and counted the presentation before asking, so the app's delegate stayed alive with nothing on screen. It now checks that UIKit took the controller first. Co-Authored-By: Claude Opus 5.5 --- .../CustomerCenterManager.swift | 12 +++++- .../CustomerCenterManagerTests.swift | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift b/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift index 29c79c2e9..61732abd5 100644 --- a/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift +++ b/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift @@ -137,10 +137,20 @@ final class CustomerCenterManager { self?.retainedDelegate = nil onDismiss?() } + presenting.present(controller, animated: presentsAnimated) + // UIKit refuses some presentations (the presenter isn't in a window, or is mid-transition) + // and only logs it, so check that it took the controller before holding on to anything. + if controller.presentingViewController == nil { + Logger.debug( + logLevel: .error, + scope: .customerCenter, + message: "The Customer Center couldn't be presented from \(type(of: presenting))." + ) + return + } retainedDelegate = delegate presentedController = controller presentCount += 1 - presenting.present(controller, animated: presentsAnimated) } /// Dismisses the presented Customer Center, if any. diff --git a/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift b/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift index 03c1f2120..922754c0b 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift @@ -84,6 +84,48 @@ struct CustomerCenterManagerTests { window.isHidden = true } + @Test("a presentation UIKit refuses doesn't keep the delegate, and the next present works") + func refusedPresentationReleasesDelegate() { + final class ProbeDelegate: CustomerCenterDelegate {} + + let container = DependencyContainer() + let manager = CustomerCenterManager(container: container) + manager.presentsAnimated = false + // Its view isn't in a window, so UIKit refuses to present from it. + let detachedPresenter = UIViewController() + + var strongDelegate: ProbeDelegate? = ProbeDelegate() + weak var weakDelegate = strongDelegate + var dismissCount = 0 + + manager.present( + configuration: nil, + from: detachedPresenter, + delegate: strongDelegate, + onDismiss: { dismissCount += 1 } + ) + strongDelegate = nil + spinRunLoop(timeout: 1) { weakDelegate == nil } + + #expect(detachedPresenter.presentedViewController == nil) + #expect(!manager.isPresented) + #expect(manager.presentCount == 0) + #expect(weakDelegate == nil) + // Nothing was shown, so nothing was dismissed. + #expect(dismissCount == 0) + + let presenter = UIViewController() + let window = makeTestWindow(rootViewController: presenter) + window.makeKeyAndVisible() + spinRunLoop(timeout: 1) { presenter.viewIfLoaded?.window != nil } + + manager.present(configuration: nil, from: presenter, delegate: nil, onDismiss: nil) + #expect(manager.isPresented) + #expect(manager.presentCount == 1) + + window.isHidden = true + } + @Test("viewDidDisappear fires didDismiss while the delegate is still retained") func viewDidDisappearFiresDelegateBeforeRelease() { final class ProbeDelegate: CustomerCenterDelegate { From 343863cf4ea380674e1fdc4de91bd2735a860368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:42:38 +0200 Subject: [PATCH 2/2] Say which part of a presentation a hostless test never reaches Co-Authored-By: Claude Opus 5.5 --- .../CustomerCenter/UIKit/CustomerCenterViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift index bfe8de379..dc7c4a776 100644 --- a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift +++ b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift @@ -47,8 +47,9 @@ public final class CustomerCenterViewController: UIHostingController