Skip to content

fix(core): restore application lifecycle events and window resolution under UIScene - #11373

Merged
NathanWalker merged 4 commits into
mainfrom
fix/multiwindow-followups
Aug 28, 2026
Merged

fix(core): restore application lifecycle events and window resolution under UIScene#11373
NathanWalker merged 4 commits into
mainfrom
fix/multiwindow-followups

Conversation

@edusperoni

@edusperoni edusperoni commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

PR Checklist


Three multi-window lifecycle fixes. The first two are regressions from #11181 on iOS with UIScene enabled, both from the same mistake: app-level UIKit APIs were used to derive state that only the window registry can answer correctly once an app adopts scenes. The third is the same class of defect on Android, pre-existing rather than a regression.

1. Application lifecycle events stopped firing in scene mode

What is the current behavior?

#11181 removed the blocks in sceneDidBecomeActive / sceneDidEnterBackground that raised app-level setInBackground() / setSuspended() for the primary scene, on the assumption that UIApplicationDidBecomeActiveNotification and UIApplicationDidEnterBackgroundNotification would drive them instead.

Those notifications do not arrive for scene-based apps, so foreground, background, resume and suspend are never raised on Application at all — including for a single-window app that simply enabled UIScene.

What is the new behavior?

The events are derived from the windows themselves, the way Android already does it with its started-activity count (application.android.ts:286-320):

  • the app enters the foreground when the first application-role window does, and leaves it when the last one goes;
  • the app resumes when the first application-role window becomes active, and suspends when the last active one resigns.

Windows in any other role (carplay, externalDisplay, embedded) never speak for the app, and cannot mask the last application window leaving. Non-scene apps are unaffected: the UIApplication notification handlers still drive them, now explicitly gated so the two paths can never double-fire.

Membership is tracked as a Set keyed by the window instance rather than as a counter, so a repeated or dropped scene callback cannot drift the aggregate. sceneDidDisconnect removes the window from both sets before the close/detach branch, so a scene that disconnects while still foregrounded releases the aggregate correctly.

Behavior note for reviewers

For a single window the aggregate collapses to that window, so all four events fire once per cycle in the same relative order as before. Two of them now fire one callback earlier:

event before #11181 this PR
foreground sceneDidBecomeActive sceneWillEnterForeground
resume sceneDidBecomeActive sceneDidBecomeActive
suspend sceneDidEnterBackground sceneWillResignActive
background sceneDidEnterBackground sceneDidEnterBackground

The old code raised foreground+resume together and background+suspend together, conflating "entered the foreground" with "became active". Splitting them matches what Android has always done, so the platforms now agree — but foreground does arrive slightly before the scene is interactive. Happy to pin both to the later callback instead if that is preferred.

The other intentional difference: the removed code was gated on isPrimary, so a secondary window never drove app state. The aggregate counts any application-role window, which is the multi-window half of the fix.

2. getWindow() asked UIKit which window NativeScript means

What is the current behavior?

getWindow() (utils/native-helper.ios.ts) resolves through NativeScriptViewFactory.getKeyWindow()UIApplication.keyWindowwindows[0]. UIApplication.keyWindow is deprecated by Apple, and under scenes key status can sit on any connected scene's window — including one NativeScript does not own.

It is the single answer to "which UIWindow does NativeScript mean", so its consumers all inherit whatever it picks: screen metrics (platform/screen/index.ios.ts), getMainScreen(), the root view controller lookup, and the embedded host resolution.

What is the new behavior?

It answers from the window registry first — the active window, then the recorded primary window — and falls back to the existing UIKit chain unchanged. One change, all consumers corrected.

A detached window is deliberately skipped. It keeps its UIWindow reference until a surface re-attaches, but that surface is gone, so handing it out would return a torn-down window after a scene reconnect. This matches the guard Application.ios.activeWindow already applies.

Before any window is registered both new sources are empty and the original chain runs verbatim, so startup is unchanged.

3. Android raised resume/suspend per activity

What is the current behavior?

On Android the background/foreground pair is aggregated across activities via activitiesCount, but resume/suspend is not — onActivityPaused raises suspend directly, and onPostResume raises resume directly. With more than one window (now reachable through Application.openWindow()), pausing one activity raises app-level suspend while another window is still on screen and active. Same defect as (1), on the other platform.

What is the new behavior?

Derived from the set of active windows, mirroring the iOS change above: resume when the first window becomes active, suspend when the last active one resigns.

Deliberately unchanged: resume is still raised from onPostResume rather than onActivityResumed (see the comment there and #6708), and only activities carrying isNativeScriptActivity take part. That guard is not replaced by a role filter — onActivityCreated registers a window for every activity in the process, third-party ones included, so those windows have role 'application' too and a role check would silently widen which activities drive suspend state.

An activity with no registered window falls back to speaking for the app only while no window holds the state, so the single-window case keeps its full pair rather than silently losing it.

activitiesCount is left as-is; converting it to the same set idiom is a mechanical follow-up, not part of this fix.

Tests

442 → 468 passing, across three new spec files. The scene-lifecycle specs observe the real foreground/background/resume/suspend events on a fresh iOSApplication and drive the real SceneDelegate methods; they cover single-window timing, first-in/last-out with two windows, non-application roles being inert, disconnect-while-foregrounded, repeated callbacks being idempotent, and that the notification handlers are silent in scene mode but still fire for non-scene apps.

No new import cycles (unchanged at android 89 / ios 88).

Not covered by CI

Device-only: that UIApplicationDidBecomeActiveNotification genuinely does not arrive under scenes (the gating makes the fix correct either way, which was the point), real UIKit callback ordering across a background/foreground cycle, and screen metrics resolving against the right window with two windows on different displays.

getWindow() is the single answer to which UIWindow NativeScript means - screen
metrics, the root view controller lookup and the embedded host all read it - and
it asked UIKit. UIApplication.keyWindow is deprecated, and under scenes key
status can sit on any connected scene's window, including one NativeScript does
not own.

It now answers from the window registry first, falling back to the UIKit lookups
unchanged. A detached window is skipped: it holds its UIWindow reference until a
surface re-attaches, but that surface is gone.
… in scene mode

Scene-based apps stopped raising the application-level lifecycle events: the
per-scene calls were replaced by UIApplication notifications that do not arrive
once an app adopts scenes. Even a single-window app lost them.

They are now derived from the windows themselves, as Android already does with
its started-activity count: the app enters the foreground when the first
application-role window does and leaves it when the last one goes, and the same
for active state. Windows in other roles never speak for the app, and the
notification handlers stay in charge for non-scene apps.
@nx-cloud

nx-cloud Bot commented Aug 28, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 2a4cdf3

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 43s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-28 22:23:12 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11373
npm i https://pkg.pr.new/@nativescript/vite@11373
npm i https://pkg.pr.new/@nativescript/webpack@11373

commit: 2a4cdf3

edusperoni and others added 2 commits August 28, 2026 19:16
…not per activity

background/foreground was already aggregated across activities, but
resume/suspend fired per activity - so with more than one window, pausing one
raised 'suspend' while another was still on screen and active.

They are now derived from the set of active windows, matching the iOS side:
'resume' when the first window becomes active, 'suspend' when the last one
resigns. Resume is still raised from onPostResume, and only NativeScript
activities take part, as before.
@NathanWalker
NathanWalker merged commit 7c5469e into main Aug 28, 2026
7 of 8 checks passed
@NathanWalker
NathanWalker deleted the fix/multiwindow-followups branch August 28, 2026 22:33
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.

2 participants