Added support for the UIScene life cycle on Apple platforms

Fixes https://github.com/libsdl-org/SDL/issues/12680
This commit is contained in:
Sam Lantinga
2025-10-30 18:19:58 -07:00
parent b6f67dd2b2
commit b46e26e65a
4 changed files with 235 additions and 7 deletions

View File

@@ -97,6 +97,8 @@ typedef Uint32 SDL_WindowID;
* uninitialized will either return the user provided value, if one was set * uninitialized will either return the user provided value, if one was set
* prior to initialization, or NULL. See docs/README-wayland.md for more * prior to initialization, or NULL. See docs/README-wayland.md for more
* information. * 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" #define SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER "SDL.video.wayland.wl_display"

View File

@@ -29,6 +29,15 @@
@end @end
API_AVAILABLE(ios(13.0))
@interface SDLUIKitSceneDelegate : NSObject <UIApplicationDelegate, UIWindowSceneDelegate>
+ (NSString *)getSceneDelegateClassName;
- (void)hideLaunchScreen;
@end
@interface SDLUIKitDelegate : NSObject <UIApplicationDelegate> @interface SDLUIKitDelegate : NSObject <UIApplicationDelegate>
+ (id)sharedAppDelegate; + (id)sharedAppDelegate;

View File

@@ -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 // Give over control to run loop, SDLUIKitDelegate will handle most things from here
@autoreleasepool { @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 // 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 @end
#endif // !SDL_PLATFORM_TVOS #endif // !SDL_PLATFORM_TVOS
@interface SDLLaunchScreenController () @interface SDLLaunchScreenController ()
#ifndef SDL_PLATFORM_TVOS #ifndef SDL_PLATFORM_TVOS
@@ -343,7 +352,170 @@ static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh)
} }
#endif // !SDL_PLATFORM_TVOS #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<UIOpenURLContext *> *)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 @implementation SDLUIKitDelegate
{ {
@@ -514,6 +686,6 @@ static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh)
return YES; return YES;
} }
@end @end // SDLUIKitDelegate
#endif // SDL_VIDEO_DRIVER_UIKIT #endif // SDL_VIDEO_DRIVER_UIKIT

View File

@@ -152,6 +152,43 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow
return true; return true;
} }
API_AVAILABLE(ios(13.0))
static UIWindowScene *GetActiveWindowScene(void)
{
if (@available(iOS 13.0, tvOS 13.0, *)) {
NSSet<UIScene *> *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) bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props)
{ {
@autoreleasepool { @autoreleasepool {
@@ -197,13 +234,21 @@ bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti
} }
#endif // !SDL_PLATFORM_TVOS #endif // !SDL_PLATFORM_TVOS
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 // ignore the size user requested, and make a fullscreen window
// !!! FIXME: can we have a smaller view?
#ifdef SDL_PLATFORM_VISIONOS #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 #else
UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds]; uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds];
#endif #endif
}
// put the window on an external display if appropriate. // put the window on an external display if appropriate.
#ifndef SDL_PLATFORM_VISIONOS #ifndef SDL_PLATFORM_VISIONOS