From 2f06100b436360bf42aa800d1f9592a0ab60ff13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 17 Oct 2025 13:43:41 +0200 Subject: [PATCH] iOS: Initial attempt at switching to SceneDelegate (UIScene) Fixes #20884 --- CMakeLists.txt | 4 +- UI/MainScreen.cpp | 2 +- cmake/Toolchains/ios.cmake | 2 +- ios/AppDelegate.h | 3 +- ios/AppDelegate.mm | 98 +++++------------------------- ios/IAPManager.mm | 8 +++ ios/PPSSPP-Info.plist | 15 +++++ ios/SceneDelegate.h | 9 +++ ios/SceneDelegate.mm | 119 +++++++++++++++++++++++++++++++++++++ 9 files changed, 173 insertions(+), 87 deletions(-) create mode 100644 ios/SceneDelegate.h create mode 100644 ios/SceneDelegate.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index f6735209f4..e4bd4a582a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1340,6 +1340,8 @@ elseif(IOS AND NOT LIBRETRO) ios/main.mm ios/AppDelegate.mm ios/AppDelegate.h + ios/SceneDelegate.mm + ios/SceneDelegate.h ios/DisplayManager.h ios/DisplayManager.mm ios/Controls.h @@ -2964,7 +2966,7 @@ endif() # packaging and code signing if(IOS AND NOT LIBRETRO) if(IOS_APP_STORE) - set(DEPLOYMENT_TARGET 12.0) + set(DEPLOYMENT_TARGET 13.0) else() set(DEPLOYMENT_TARGET 11.0) endif() diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index c8ee057c44..49a6c8ac39 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1531,7 +1531,7 @@ void LaunchBuyGold(ScreenManager *screenManager) { screenManager->push(new IAPScreen()); } else { #if PPSSPP_PLATFORM(IOS_APP_STORE) - System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://apps.apple.com/us/app/ppsspp-gold-psp-emulator/id6502287918"); + System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/buygold_ios"); #elif PPSSPP_PLATFORM(ANDROID) System_LaunchUrl(LaunchUrlType::BROWSER_URL, "market://details?id=org.ppsspp.ppssppgold"); #else diff --git a/cmake/Toolchains/ios.cmake b/cmake/Toolchains/ios.cmake index 5ffad1d907..df3e460ec3 100644 --- a/cmake/Toolchains/ios.cmake +++ b/cmake/Toolchains/ios.cmake @@ -12,7 +12,7 @@ # PPSSPP platform flags set(MOBILE_DEVICE ON) set(USING_GLES2 ON) -set(IPHONEOS_DEPLOYMENT_TARGET 12.0) +set(IPHONEOS_DEPLOYMENT_TARGET 13.0) add_definitions( -DGL_ETC1_RGB8_OES=0 -U__STRICT_ANSI__ diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index 06ab74d32a..ecadddacff 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -8,8 +8,7 @@ @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) UIScreen *screen; - -@property (strong, nonatomic) id viewController; +@property (nonatomic, strong) NSDictionary *launchOptions; - (void)restart:(const char *)args; - (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index 6d7d9b6785..0b9a30d0b0 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -1,7 +1,5 @@ #import "AppDelegate.h" -#import "ViewControllerCommon.h" -#import "ViewController.h" -#import "ViewControllerMetal.h" +#import "SceneDelegate.h" #import "iOSCoreAudio.h" #import "Common/System/System.h" #import "Common/System/NativeApp.h" @@ -11,6 +9,12 @@ #import "IAPManager.h" #import +#import + +// TODO: Unfortunate hack to force link SceneDelegate class. +// This is necessary because otherwise classes from static libraries aren't loaded. +// We should move all the iOS code into the main binary directly to avoid this problem. +__attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate class]; @implementation AppDelegate @@ -77,49 +81,21 @@ } -(BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - int argc = 1; - char *argv[5]{}; - NSURL *nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]; - - if (nsUrl != nullptr && nsUrl.isFileURL) { - NSString *nsString = nsUrl.path; - const char *string = nsString.UTF8String; - argv[argc++] = (char*)string; + self.launchOptions = launchOptions; + // Make sure SceneDelegate class is loaded + Class cls = objc_getClass("SceneDelegate"); + if (!cls) { + NSLog(@"⚠️ SceneDelegate not found via objc_getClass"); + } else { + NSLog(@"✅ SceneDelegate loaded via objc_getClass"); } - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil]; - #if PPSSPP_PLATFORM(IOS_APP_STORE) [IAPManager sharedIAPManager]; // Kick off the IAPManager early. #endif // IOS_APP_STORE - return [self launchPPSSPP:argc argv:argv]; -} - -- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; -{ - NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; - NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"]; - NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL); - - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - - // Choose viewcontroller depending on backend. - if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) { - PPSSPPViewControllerMetal *vc = [[PPSSPPViewControllerMetal alloc] init]; - - self.viewController = vc; - self.window.rootViewController = vc; - - } else { - PPSSPPViewControllerGL *vc = [[PPSSPPViewControllerGL alloc] init]; - // Here we can switch viewcontroller depending on backend. - self.viewController = vc; - self.window.rootViewController = vc; - } - - [self.window makeKeyAndVisible]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil]; return YES; } @@ -128,49 +104,7 @@ [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)restart:(const char*)restartArgs { - INFO_LOG(Log::G3D, "Restart requested: %s", restartArgs); - [self.viewController willResignActive]; - [self.viewController shutdown]; - self.window.rootViewController = nil; - self.viewController = nil; - - // App was requested to restart, probably. - INFO_LOG(Log::G3D, "viewController nilled"); - - NativeShutdown(); - - // TODO: Ignoring the command line for now. - // Hoping that overwriting the viewController works as expected... - [self launchPPSSPP:0 argv:nullptr]; - [self.viewController didBecomeActive]; -} - -- (void)applicationWillResignActive:(UIApplication *)application { - INFO_LOG(Log::G3D, "willResignActive"); - - [self.viewController willResignActive]; - - if (g_Config.bEnableSound) { - iOSCoreAudioShutdown(); - } - - System_PostUIMessage(UIMessage::LOST_FOCUS); -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { - INFO_LOG(Log::G3D, "didBecomeActive"); - if (g_Config.bEnableSound) { - iOSCoreAudioInit(); - } - - System_PostUIMessage(UIMessage::GOT_FOCUS); - [self.viewController didBecomeActive]; -} - - (void)applicationWillTerminate:(UIApplication *)application { - // Seems like a bad idea. - // exit(0); } @end diff --git a/ios/IAPManager.mm b/ios/IAPManager.mm index 505cb2c3a9..e40cbb544d 100644 --- a/ios/IAPManager.mm +++ b/ios/IAPManager.mm @@ -215,4 +215,12 @@ static bool SafeStringEqual(NSString *a, NSString *b) { } } +- (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response { + NSLog(@"Ignoring [productsRequest]"); +} + +- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray *)transactions { + NSLog(@"Ignoring [paymentQueue]"); +} + @end diff --git a/ios/PPSSPP-Info.plist b/ios/PPSSPP-Info.plist index bbc2f42837..c23fc0655d 100644 --- a/ios/PPSSPP-Info.plist +++ b/ios/PPSSPP-Info.plist @@ -206,5 +206,20 @@ + UIApplicationSceneManifest + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + + + + diff --git a/ios/SceneDelegate.h b/ios/SceneDelegate.h new file mode 100644 index 0000000000..318e17628d --- /dev/null +++ b/ios/SceneDelegate.h @@ -0,0 +1,9 @@ +// SceneDelegate.h +#import + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow * window; +@property (strong, nonatomic) UIViewController *viewController; + +@end diff --git a/ios/SceneDelegate.mm b/ios/SceneDelegate.mm new file mode 100644 index 0000000000..e86c55dff1 --- /dev/null +++ b/ios/SceneDelegate.mm @@ -0,0 +1,119 @@ +// SceneDelegate.mm + +#import "SceneDelegate.h" +#import "AppDelegate.h" + +#import "iOSCoreAudio.h" +#import "ViewControllerCommon.h" +#import "ViewController.h" +#import "ViewControllerMetal.h" +#import "Common/System/NativeApp.h" +#import "Common/System/System.h" +#import "Core/System.h" +#import "Core/Config.h" +#import "Common/Log.h" +#import "IAPManager.h" + +#import + +@implementation SceneDelegate + ++ (void)load { + NSLog(@"✅ SceneDelegate class was loaded!"); +} + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { + if (![scene isKindOfClass:[UIWindowScene class]]) { + return; + } + + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (self.window) { + NSLog(@"✅ Window already exists, not creating again!"); + return; + } + + AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; + NSDictionary *launchOptions = appDelegate.launchOptions; + + int argc = 1; + char *argv[5]{}; + NSURL *nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]; + + if (nsUrl != nullptr && nsUrl.isFileURL) { + NSString *nsString = nsUrl.path; + const char *string = nsString.UTF8String; + argv[argc++] = (char*)string; + } + + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"]; + NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL); + + self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; +// self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + + // Choose viewcontroller depending on backend. + if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) { + PPSSPPViewControllerMetal *vc = [[PPSSPPViewControllerMetal alloc] init]; + + self.viewController = vc; + self.window.rootViewController = vc; + + } else { + PPSSPPViewControllerGL *vc = [[PPSSPPViewControllerGL alloc] init]; + // Here we can switch viewcontroller depending on backend. + self.viewController = vc; + self.window.rootViewController = vc; + } + + [self.window makeKeyAndVisible]; +} + +- (void)restart:(const char*)restartArgs { + INFO_LOG(Log::G3D, "SceneDelegate: Restart requested: %s", restartArgs); + + // Notify current view controller + [self.viewController willResignActive]; + [self.viewController shutdown]; + + // Remove the current root view controller + self.window.rootViewController = nil; + self.viewController = nil; + + INFO_LOG(Log::G3D, "SceneDelegate: viewController nilled"); + + // Shut down native systems + NativeShutdown(); + + // Launch a fresh instance + [self launchPPSSPP:0 argv:nullptr]; + + // Notify new view controller + [self.viewController didBecomeActive]; +} + +- (void)sceneWillResignActive:(UIScene *)scene { + INFO_LOG(Log::G3D, "sceneWillResignActive"); + + [self.viewController willResignActive]; + + if (g_Config.bEnableSound) { + iOSCoreAudioShutdown(); + } + + System_PostUIMessage(UIMessage::LOST_FOCUS); +} + +- (void)sceneDidBecomeActive:(UIScene *)scene { + INFO_LOG(Log::G3D, "sceneDidBecomeActive"); + + if (g_Config.bEnableSound) { + iOSCoreAudioInit(); + } + + System_PostUIMessage(UIMessage::GOT_FOCUS); + + [self.viewController didBecomeActive]; +} +@end