From b46e26e65aac8f7d5b5c83d43ea99a507c093930 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Thu, 30 Oct 2025 18:19:58 -0700 Subject: [PATCH] Added support for the UIScene life cycle on Apple platforms Fixes https://github.com/libsdl-org/SDL/issues/12680 --- include/SDL3/SDL_video.h | 2 + src/video/uikit/SDL_uikitappdelegate.h | 9 ++ src/video/uikit/SDL_uikitappdelegate.m | 178 ++++++++++++++++++++++++- src/video/uikit/SDL_uikitwindow.m | 53 +++++++- 4 files changed, 235 insertions(+), 7 deletions(-) diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index d727457191..7e58bc12af 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -97,6 +97,8 @@ typedef Uint32 SDL_WindowID; * uninitialized will either return the user provided value, if one was set * prior to initialization, or NULL. See docs/README-wayland.md for more * information. + * + * \since This macro is available since SDL 3.2.0. */ #define SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER "SDL.video.wayland.wl_display" diff --git a/src/video/uikit/SDL_uikitappdelegate.h b/src/video/uikit/SDL_uikitappdelegate.h index 77ccbfdd2c..25dfc9d3c9 100644 --- a/src/video/uikit/SDL_uikitappdelegate.h +++ b/src/video/uikit/SDL_uikitappdelegate.h @@ -29,6 +29,15 @@ @end +API_AVAILABLE(ios(13.0)) +@interface SDLUIKitSceneDelegate : NSObject + ++ (NSString *)getSceneDelegateClassName; + +- (void)hideLaunchScreen; + +@end + @interface SDLUIKitDelegate : NSObject + (id)sharedAppDelegate; diff --git a/src/video/uikit/SDL_uikitappdelegate.m b/src/video/uikit/SDL_uikitappdelegate.m index 3c1bb37366..2af7165890 100644 --- a/src/video/uikit/SDL_uikitappdelegate.m +++ b/src/video/uikit/SDL_uikitappdelegate.m @@ -59,7 +59,15 @@ int SDL_RunApp(int argc, char *argv[], SDL_main_func mainFunction, void *reserve // Give over control to run loop, SDLUIKitDelegate will handle most things from here @autoreleasepool { - UIApplicationMain(argc, argv, nil, [SDLUIKitDelegate getAppDelegateClassName]); + NSString *name = nil; + + if (@available(iOS 13.0, tvOS 13.0, *)) { + name = [SDLUIKitSceneDelegate getSceneDelegateClassName]; + } + if (!name) { + name = [SDLUIKitDelegate getAppDelegateClassName]; + } + UIApplicationMain(argc, argv, nil, name); } // free the memory we used to hold copies of argc and argv @@ -162,6 +170,7 @@ static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh) @end #endif // !SDL_PLATFORM_TVOS + @interface SDLLaunchScreenController () #ifndef SDL_PLATFORM_TVOS @@ -343,7 +352,170 @@ static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh) } #endif // !SDL_PLATFORM_TVOS -@end +@end // SDLLaunchScreenController + + +API_AVAILABLE(ios(13.0)) +@implementation SDLUIKitSceneDelegate +{ + UIWindow *launchWindow; +} + ++ (NSString *)getSceneDelegateClassName +{ + return @"SDLUIKitSceneDelegate"; +} + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions +{ + if (![scene isKindOfClass:[UIWindowScene class]]) { + return; + } + + UIWindowScene *windowScene = (UIWindowScene *)scene; + windowScene.delegate = self; + + NSBundle *bundle = [NSBundle mainBundle]; + +#ifdef SDL_IPHONE_LAUNCHSCREEN + UIViewController *vc = nil; + NSString *screenname = nil; + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + screenname = [bundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"]; + + if (screenname) { + @try { + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:screenname bundle:bundle]; + __auto_type storyboardVc = [storyboard instantiateInitialViewController]; + vc = [[SDLLaunchStoryboardViewController alloc] initWithStoryboardViewController:storyboardVc]; + } + @catch (NSException *exception) { + // Do nothing (there's more code to execute below). + } + } +#endif + + if (vc == nil) { + vc = [[SDLLaunchScreenController alloc] initWithNibName:screenname bundle:bundle]; + } + + if (vc.view) { +#ifdef SDL_PLATFORM_VISIONOS + CGRect viewFrame = CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT); +#else + CGRect viewFrame = windowScene.coordinateSpace.bounds; +#endif + launchWindow = [[UIWindow alloc] initWithWindowScene:windowScene]; + launchWindow.frame = viewFrame; + + launchWindow.windowLevel = UIWindowLevelNormal + 1.0; + launchWindow.hidden = NO; + launchWindow.rootViewController = vc; + } +#endif + + // Set working directory to resource path + [[NSFileManager defaultManager] changeCurrentDirectoryPath:[bundle resourcePath]]; + + // Handle any connection options (like opening URLs) + for (NSUserActivity *activity in connectionOptions.userActivities) { + if (activity.webpageURL) { + [self handleURL:activity.webpageURL]; + } + } + + for (UIOpenURLContext *urlContext in connectionOptions.URLContexts) { + [self handleURL:urlContext.URL]; + } + + SDL_SetMainReady(); + [self performSelector:@selector(postFinishLaunch) withObject:nil afterDelay:0.0]; +} + +- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts +{ + for (UIOpenURLContext *context in URLContexts) { + [self handleURL:context.URL]; + } +} + +- (void)sceneDidBecomeActive:(UIScene *)scene +{ + SDL_OnApplicationDidEnterForeground(); +} + +- (void)sceneWillResignActive:(UIScene *)scene +{ + SDL_OnApplicationWillEnterBackground(); +} + +- (void)sceneWillEnterForeground:(UIScene *)scene +{ + SDL_OnApplicationWillEnterForeground(); +} + +- (void)sceneDidEnterBackground:(UIScene *)scene +{ + SDL_OnApplicationDidEnterBackground(); +} + +- (void)handleURL:(NSURL *)url +{ + const char *sourceApplicationCString = NULL; + NSURL *fileURL = url.filePathURL; + if (fileURL != nil) { + SDL_SendDropFile(NULL, sourceApplicationCString, fileURL.path.UTF8String); + } else { + SDL_SendDropFile(NULL, sourceApplicationCString, url.absoluteString.UTF8String); + } + SDL_SendDropComplete(NULL); +} + +- (void)hideLaunchScreen +{ + UIWindow *window = launchWindow; + + if (!window || window.hidden) { + return; + } + + launchWindow = nil; + + [UIView animateWithDuration:0.2 + animations:^{ + window.alpha = 0.0; + } + completion:^(BOOL finished) { + window.hidden = YES; + UIKit_ForceUpdateHomeIndicator(); + }]; +} + +- (void)postFinishLaunch +{ + [self performSelector:@selector(hideLaunchScreen) withObject:nil afterDelay:0.0]; + + SDL_SetiOSEventPump(true); + exit_status = forward_main(forward_argc, forward_argv); + SDL_SetiOSEventPump(false); + + if (launchWindow) { + launchWindow.hidden = YES; + launchWindow = nil; + } +} + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)) +{ + // This doesn't appear to be called, but it needs to be implemented to signal that we support the UIScene life cycle + UISceneConfiguration *config = [[UISceneConfiguration alloc] initWithName:@"SDLSceneConfiguration" sessionRole:connectingSceneSession.role]; + config.delegateClass = [SDLUIKitSceneDelegate class]; + return config; +} + +@end // SDLUIKitSceneDelegate + @implementation SDLUIKitDelegate { @@ -514,6 +686,6 @@ static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh) return YES; } -@end +@end // SDLUIKitDelegate #endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/src/video/uikit/SDL_uikitwindow.m b/src/video/uikit/SDL_uikitwindow.m index 2b258b1913..5fa0a3173c 100644 --- a/src/video/uikit/SDL_uikitwindow.m +++ b/src/video/uikit/SDL_uikitwindow.m @@ -152,6 +152,43 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow return true; } +API_AVAILABLE(ios(13.0)) +static UIWindowScene *GetActiveWindowScene(void) +{ + if (@available(iOS 13.0, tvOS 13.0, *)) { + NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; + + // First, try to find an active foreground scene + for (UIScene *scene in connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.activationState == UISceneActivationStateForegroundActive) { + return windowScene; + } + } + } + + // If no active scene, return any foreground scene + for (UIScene *scene in connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.activationState == UISceneActivationStateForegroundInactive) { + return windowScene; + } + } + } + + // Last resort: return first window scene + for (UIScene *scene in connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + return (UIWindowScene *)scene; + } + } + } + + return nil; +} + bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) { @autoreleasepool { @@ -197,13 +234,21 @@ bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti } #endif // !SDL_PLATFORM_TVOS - // ignore the size user requested, and make a fullscreen window - // !!! FIXME: can we have a smaller view? + UIWindow *uiwindow = nil; + if (@available(iOS 13.0, tvOS 13.0, *)) { + UIWindowScene *scene = GetActiveWindowScene(); + if (scene) { + uiwindow = [[SDL_uikitwindow alloc] initWithWindowScene:scene]; + } + } + if (!uiwindow) { + // ignore the size user requested, and make a fullscreen window #ifdef SDL_PLATFORM_VISIONOS - UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:CGRectMake(window->x, window->y, window->w, window->h)]; + uiwindow = [[SDL_uikitwindow alloc] initWithFrame:CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT)]; #else - UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds]; + uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds]; #endif + } // put the window on an external display if appropriate. #ifndef SDL_PLATFORM_VISIONOS