Skip to content

ios: support iOS 27 UIScene lifecycle (TN3187)#634

Open
benface wants to merge 2 commits into
not-fl3:masterfrom
benface:ios27-uiscene
Open

ios: support iOS 27 UIScene lifecycle (TN3187)#634
benface wants to merge 2 commits into
not-fl3:masterfrom
benface:ios27-uiscene

Conversation

@benface

@benface benface commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Problem

The iOS 27 SDK enforces the UIScene lifecycle for window management (per Apple's TN3187). Two consequences for apps built with the new SDK:

  1. Apps that don't declare UIApplicationSceneManifest in Info.plist refuse to launch with the error "UIScene life cycle is required for apps built with this SDK".
  2. A UIWindow created via initWithFrame: from application:didFinishLaunchingWithOptions: is no longer auto-attached to a scene, so it stays orphan and invisible. The user sees a black screen after the splash; the GL/Metal view is never visible.

miniquad's current iOS path hits both. Note that already-shipped apps run fine on iOS 27 — the enforcement is SDK-bound, not runtime-bound. The bug only appears when rebuilding against Xcode 27 / the iOS 27 SDK.

Fix

This patch:

  1. Defers UIWindow creation from did_finish_launching_with_options to a UISceneWillConnectNotification observer (scene_will_connect). The observer creates the window via initWithWindowScene: with the scene iOS provides, then adopts the view + view controller that didFinishLaunching had already set up. The view + view_ctrl are stashed in thread_local storage between the two callbacks (both run on the main thread).

  2. Observes UISceneDidActivateNotification to re-send Message::Resume. Without this, iOS 27 fires a spurious applicationWillResignActive: during the scene-attach handoff (legacy lifecycle compat path) which sends Message::Pause and blocks the render thread on rx.recv(), with no follow-up applicationDidBecomeActive: to wake it back up. The symptom is exactly one frame of work happening (whatever ran synchronously up to the first next_frame().await), then nothing.

The patch is iOS-only and additive — pre-iOS-13 paths aren't touched, and UISceneWillConnectNotification has been available since iOS 13.

Caller responsibilities

Apps using a miniquad with this patch must declare UIApplicationSceneManifest in Info.plist with at least one scene configuration, including a UISceneDelegateClassName pointing to either a stub UIWindowSceneDelegate-conforming class or the platform-provided default. Without the manifest, iOS 27 still refuses to launch the app — that part of the enforcement is unrelated to the patch.

A minimal stub delegate is enough; this patch handles all window/view wiring itself, so the scene delegate doesn't need any methods implemented:

@interface MySceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

@implementation MySceneDelegate
@end

Tested on

iPhone running iOS 27 beta. Verified: launches past splash, window is visible, render thread runs continuously, asset loading completes, game renders + responds to input. Tested against the unpatched miniquad@0.4.10 baseline (black screen / hang after first frame) for comparison.

Why not a separate version bump

Happy to bump if preferred — this branch is built on top of the last commit at version = "0.4.10" on master to keep the diff minimal. Patch is ~80 lines net.

benface added 2 commits June 13, 2026 19:27
iOS 27 SDK enforces the UIScene lifecycle for window management.
UIWindow created via `initWithFrame:` from AppDelegate's
`didFinishLaunching` is no longer auto-attached to a scene, so it
stays invisible. Apps that don't declare `UIApplicationSceneManifest`
in Info.plist also refuse to launch.

Defer window creation to a `UISceneWillConnectNotification` observer
and use `initWithWindowScene:` with the scene iOS provides. The view
and view controller created during `didFinishLaunching` are stashed
in thread_local storage for the observer to adopt.

Also observe `UISceneDidActivateNotification` to re-send
`Message::Resume`. iOS 27 fires `applicationWillResignActive:` during
the scene-attach handoff (legacy lifecycle compat path) which sends
Pause and blocks the render thread, with no follow-up
`applicationDidBecomeActive:` to wake it back up.

Apps using this patched miniquad must declare
`UIApplicationSceneManifest` in Info.plist or iOS 27 will refuse to
launch them.
- Merge `PENDING_SCENE_VIEW` + `PENDING_SCENE_VIEW_CTRL` into a single
  `PENDING_SCENE_VIEW_AND_CTRL: Option<(ObjcId, ObjcId)>` thread_local.
  The two values are always set together and always taken together;
  a single tuple makes the invariant structural and drops one `match`
  arm at the take site.
- Reorder `scene_will_connect` and `scene_did_activate` so the
  declaration order matches the lifecycle order (will-connect first,
  did-activate second). Selector registrations and observer hookups
  were already in this order.

No functional change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant