I ended up finding a solution for UIKit through the hints from @jpdev001. The fix involves two key components:
Creating a hidden UIWindow that provides the necessary context for StoreKit operations. This window is kept at a lower window level but maintains key status.
Wrapping the presenting view controller in a UINavigationController, which iOS 18.2's StoreKit requires for proper purchase flow.
I created a reusable protocol and utility function that handles this automatically. The function takes care of creating the hidden window, managing the navigation controller setup, and setting the requested presentation styles.
protocol Navigatable: UIViewController {
var displayedWindow: UIWindow? { get set }
}
extension UIViewController {
func presentWithStoreKitSupport<T: Navigatable>(
from presentingView: UIViewController,
viewController: T,
presentationStyle: UIModalPresentationStyle,
isModalInPresentation: Bool = false,
transitionStyle: UIModalTransitionStyle? = nil,
animated: Bool = true
) {
// Create navigation controller
let navigationController = UINavigationController(rootViewController: viewController)
navigationController.isNavigationBarHidden = true
navigationController.modalPresentationStyle = presentationStyle
navigationController.isModalInPresentation = isModalInPresentation
if let transitionStyle = transitionStyle {
navigationController.modalTransitionStyle = transitionStyle
}
// Create StoreKit context window
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
let purchaseWindow = UIWindow(windowScene: windowScene)
purchaseWindow.isHidden = true
purchaseWindow.windowLevel = .normal - 1
purchaseWindow.rootViewController = UIViewController()
purchaseWindow.makeKeyAndVisible()
// Store window reference
viewController.displayedWindow = purchaseWindow
// Present the navigation controller
presentingView.present(navigationController, animated: animated)
}
}
}
The transition to the respective view can be implemented the following way:
view.presentWithStoreKitSupport(
from: view, // the main view
viewController: navigatingView, // view we intend to navigate to
presentationStyle: .fullScreen
)
Using this approach for the views that include in-app purchases fixed the issue for me. The solution also still works for older iOS Versions (tested with iOS 15 an 17). Also tested on multiple physical devices (iPhone 12 Pro and 15 Pro).