feat: add cross-platform NativeWindow implementation - #11181
Conversation
|
View your CI Pipeline Execution ↗ for commit 49b7f19
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
|
Great start here, it might be helpful to setup a new demo page in toolbox or repurpose |
|
|
||
| // --- Typed event overloads --- | ||
|
|
||
| on(event: 'activate', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; |
There was a problem hiding this comment.
those should go into a native-window.d.ts file (which is missing by the way)
| onSceneConfiguration: ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null; | ||
|
|
||
| /** | ||
| * @deprecated Listen on a NativeWindow instance instead. |
There was a problem hiding this comment.
i dont think we should mark those as deprecated. There is a lot of power in having Application events.
It is the only place where we can have all windows event from one single listener.
same for orientation and all other events.
I think it is great to have them both in NativeWindow and Application.
plus we ll never be able to remove them. Application events are deeply used in all existing apps and plugins.
| getRegisteredBroadcastReceivers(intentFilter: string): android.content.BroadcastReceiver[]; | ||
|
|
||
| /** | ||
| * @deprecated Listen on a NativeWindow instance instead. |
There was a problem hiding this comment.
Same thing i would not deprecate those, only explain you can do it through NativeWindw
There was a problem hiding this comment.
should the android NativeWindow listen to configuration change, just like we do in application?
It should store it s configuration, font scale, orientation... use them for accessors but also notify the NativeWindow about them. Just like we do in Application.
maybe we can even have Application plugged to use accessors, events from the main window?
Not sure about this because in application we use context and not activity i think, not sure what difference it makes for configurations like orientation, font scale ...
| } as AndroidActivityNewIntentEventData); | ||
| } | ||
|
|
||
| // @deprecated - Bridge to Application.android for backward compat |
There was a problem hiding this comment.
i wouldnt deprecate here again. It is really nice to have that
- Introduced NativeWindow class to manage platform-specific window behavior for iOS and Android. - Added interfaces and common functionality for NativeWindow, including event handling and lifecycle management. - Implemented Android-specific NativeWindow logic to wrap AppCompatActivity and manage its lifecycle. - Implemented iOS-specific NativeWindow logic to wrap UIWindowScene and UIWindow, handling view controller setup and trait collection changes. - Updated core index files to export new NativeWindow functionality.
…ling - Refactored NativeWindow to separate platform-specific implementations for Android and iOS. - Introduced AndroidNativeWindow and IOSNativeWindow classes extending a common NativeWindow base class. - Updated lifecycle event notifications to emit events directly from NativeWindow instances instead of the Application class. - Deprecated direct event listeners on the Application class in favor of listening on NativeWindow instances. - Removed redundant index files for native-window on both Android and iOS platforms. - Enhanced type definitions for NativeWindow events and interfaces to improve clarity and maintainability.
44fbfb6 to
cdd182b
Compare
- restore missing </dict> in toolbox Info.plist scene manifest - stop _destroy() emitting a second 'close' event (callers already notify) - promote primary windows via _setIsPrimary() instead of an any-cast - _getWindows() returns a copy instead of the live internal array - read activity callbacks via the CALLBACKS constant - make WindowCloseEventData.window required - fix sceneContentSetup deprecation text referencing a nonexistent event
…heir payloads The activity*/scene* events on Application are aggregate APIs that fire for every window; args.window identifies which one. SceneEventData's native UIWindow moves to uiWindow so that a 'window' payload key always means a NativeWindow.
…ming collisions WindowBase carries identity, role, state, lifecycle events and the native accessors; NativeWindow adds the view-carrying half. Surfaces without a NativeScript view tree (CarPlay, external displays) can extend WindowBase. BREAKING CHANGE: NativeWindow.iosWindow is now .ios and its window property is .uiWindow; NativeWindow.androidWindow is now .android. getWindows() is role-filtered and defaults to application and embedded windows.
…exit rewire A window now survives its native surface going away: iOS keys identity off UISceneSession.persistentIdentifier, Android mints a UUID persisted in savedInstanceState. A scene disconnect or an activity recreation detaches the window - it stays registered and keeps its listeners - while a real close ends the session, fires 'close' exactly once and then clears the instance listeners. Android 'exit' now fires when the last window goes away rather than for any finishing activity; iOS 'exit' remains process termination.
…nership inversion) Every iOS app now registers at least one window - scene, non-scene and embedded alike - and all content flows through NativeWindow.setContent(). The app-level root view state (getRootView(), the global root view and the initRootView event) mirrors whatever the primary window shows. Embedded windows hand their controller to the embedder delegate and never touch rootViewController or makeKeyAndVisible: the host app owns the window.
resetActivityContent() accepts an explicit view, so NativeWindow.setContent() no longer has its argument discarded and rebuilt from the main entry. Root views built by the activity pipeline are handed to their window through _adoptRootView(), which records the view and fires contentLoaded without redoing the setup the pipeline already performed. Embedded hosts get a window with role 'embedded'.
…cation 'launch' conflated process initialization with building the first window's UI, which cannot work for additional windows or for background launches. 'ready' now covers initialization - fired exactly once per JS context, never deferred, always before the first windowOpen - while setWindowContentResolver() supplies each window's UI on demand. 'launch' keeps working as a bridge for the first view-carrying window, including its 'root' three-state contract. shouldDelayLaunchEvent is a deprecated no-op.
…tions in scene mode App-level suspend/resume were driven by the primary scene, so a multi-scene app reported itself suspended while other scenes were still on screen. They now follow UIApplication's own foreground/background notifications, which describe the whole app. Per-window state stays available through NativeWindow events.
…m openWindow (Android experimental) The registry, its events and primary promotion had one copy per platform; they now live on ApplicationCommon, with protected hooks for the iOS-specific bookkeeping. openWindow() takes WindowOpenOptions on both platforms. Android openWindow() launches a real second activity. It is experimental: whether a new window appears depends on the manifest launchMode and on OEM recents behavior.
Orientation, system appearance and layout direction are per-window: each NativeWindow reads them live from its own surface, raises its own changed events, and scopes its CSS classes to its own root view and that root's modals instead of the process-wide system class list. Application-level getters and events continue to reflect the primary window. Accessibility CSS is applied per root view, so windows beyond the first are no longer skipped.
…ttern Adds a curated public surface for native-window - WindowBase, NativeWindow, the event constants and payload types, the resolver types - keeping internals and the concrete platform classes out of the published typings. Also brings application.d.ts back in line with the implementation: the platform classes declared on() as method overloads, which hid ApplicationCommon's own handler surface, and several referenced types were never imported and silently resolved to any.
Covers the window lifecycle contract (attach/detach/re-attach/close, close firing once, listeners cleared only after close and surviving a detach), the registry's role filtering and primary promotion, the content resolution chain including the launch bridge's three-state root and app CSS loading on every path, and the Android trait converters and session-id round trip.
Rewrites the multiple-scenes page on the shared window API: windows are opened
with Application.openWindow({ data }), their UI comes from a
setWindowContentResolver, and each row shows live id/role/state/traits driven
by that window's own events. Gating is by capability rather than platform, so
the page renders and explains itself on Android too.
…none remain unregisterBroadcastReceiver empties the entry rather than deleting it, so the optional chain guarded the array but not its first element and the getter threw for a filter that had been registered and then unregistered.
…hods Once an app adopts scenes, UIKit stops calling applicationOpenURLOptions, applicationContinueUserActivityRestorationHandler and applicationPerformActionForShortcutItemCompletionHandler and delivers them to the scene delegate instead, so handlers registered through addDelegateHandler() stopped firing - deep links, universal links, Handoff and quick actions. The scene delegate now implements all three, raising a per-window event and its Application-level bridge, then forwarding to the legacy delegate method so existing handlers keep working. A quick action's completion handler is shared between listeners and the legacy handler and reports at most once.
…window adopts it Views had no supported route back to the window showing them. getNativeWindow() walks up to the root view - through the presenting view of any modal - and reads the window that owns it, resolving on every call so a view re-parented into another tree reports where it moved to. iOS lets a UIView change windows, and setContent() only ever reset its own previous root, so two windows could claim the same root view and UIKit could be left holding a rootViewController it no longer hosts. A window now releases its root when another adopts it, without tearing the view down.
Reading the activity callbacks key pulled native-window.android into ui/frame/frame-helper-for-android, which reaches back through fragment.transitions.android to application.android. The key and the frame id now live in a module of their own with no imports, so both sides can read them without the cycle. The frame-common import alongside it was types only and is now an import type; NavigationEntry there was unused.
Both back paths resolved the pressing activity's own root view and then handed navigation to Frame.goBack(), which walks the process-global frame stack - so with two windows a back press in one window could navigate the other, whichever had navigated most recently. Frame.goBack() now takes an optional frame to go back from, defaulting to the topmost as before, and the Android back paths pass the frame they already resolved. The terminal pop is skipped for a frame that is not at the top of the stack, which would otherwise throw.
The frame stack is process-global and a frame reaches its top on navigate, so with more than one window Frame.topmost() returned whichever frame navigated most recently anywhere - sending dialogs, keyboard dismissal and back delegation to the wrong window. Frame.topmost() now takes an optional window and defaults to the newly exposed Application.activeWindow. A frame that belongs to no window still wins outright, so single-window behaviour is unchanged in every arrangement.
|
Other than @farfromrefug points that need to be considered, it looks good to me and will definitely bring lots of new possibilities in core. |
PR Checklist
The concept
The developer chooses how to handle this. From an Angular background:
bootstrapApplication(...)— "root" services are per-window, "platform" services are shared.createComponent— "root" services are shared, navigation is similar to modal navigation.Motivations: iOS 27 makes UIScene mandatory; Android's
singleTaskrequirement exists only because the framework binds everything to one root activity; CarPlay/Android Auto need a home; Windows runtimes are coming.What is the current behavior?
NativeWindowexisted only as a draft POC. Window content was still owned by the platform application classes, windows died whenever their native surface went away, the window registry was duplicated per platform, andlaunchconflated "the process started" with "build the first window's UI" — which cannot work for a second window or a background launch.What is the new behavior?
The POC is now a complete implementation, landed as 14 individually-typechecking commits.
WindowBase/NativeWindowsplit.WindowBasecarries identity, role, state, lifecycle and native accessors;NativeWindowadds the view-carrying half. Surfaces with no NativeScript view tree (CarPlay, external displays) can extendWindowBaselater. Roles:application | embedded | carplay | externalDisplay.UISceneSession.persistentIdentifier(and handlesdidDiscardSceneSessions), Android mints a UUID persisted insavedInstanceState. A scene disconnect or activity recreation raisesdetached— the window stays registered and keeps its listeners — while a real close raisescloseexactly once and then clears the instance's listeners so apps don't leak handlers.window.setContent(). Every app registers at least one window: scene, non-scene, and embedded alike. App-level root state (getRootView(), the global root view,initRootView) now mirrors the primary window instead of owning it.ready+ content resolver.readyfires exactly once per JS context, is never deferred (including background launches), and always precedes the firstwindowOpen.Application.setWindowContentResolver()supplies each window's UI on demand.launchkeeps working as a bridge for the first view-carrying window, including itsrootthree-state contract.primaryWindow,getWindows(role?),getWindowById(),openWindow(options?)and the window events live onApplicationCommon, with protected hooks for iOS-specific bookkeeping.openWindowis implemented for real (new activity via intent) and flagged experimental.native-windowfollowing theui/framehand-writtenindex.d.tsconvention, keeping internals and the concrete platform classes out of the published surface.Test suite: 212 → 287 tests across 19 files.
Bugs fixed along the way
exitcould be lost entirely. The last-window check initially went where the old code lived, in a callback that only runs for NativeScript activities — but windows are registered for every activity, so a third-party SDK activity outliving the main one meantexitnever fired.app.csswould never load for resolver-only apps.loadAppCss()is reachable from exactly one place, insidenotifyLaunch(), which the resolver chain skips.window.setContent(view)discarded its argument on Android and rebuilt from the main entry._detach()disposed native views withoutcallUnloaded, sounloadedhandlers never ran.classNamePropertydropped window-scoped CSS classes — it clearscssClassesand reseeds from the global list, so aclassNamechange on a root or modal-root view lostns-dark/ns-landscape._getRootModalViews()is process-wide).Application.ios.on('ready' | 'suspend' | …) did not typecheck— both platform classes declaredon()as method overloads, replacingApplicationCommon's handler surface. Seven types inapplication.d.tswere referenced but never imported and silently resolved toany.launchdispatch would tear down everylaunchlistener (events.off('launch', null)).autoSystemAppearanceChangedwas bypassed by the per-window CSS path; accessibility CSS init was one-shot so windows 2+ never got a11y/font-scale classes;closefired twice per teardown;_getWindows()leaked the live internal array; the toolboxInfo.plistwas malformed XML (failedplutil -lint).Known limitations
exitindefinitely. Suppression is correct for the normal recreation case and there is no clean discriminator.NativeWindowrepeatsWindowBase's wholeon()overload set — TypeScript requires a derived overload set to cover every base overload, and a catch-all cannot stand in for theSceneEventData/AndroidActivity*shapes.displayedfires on the primary window only; secondaries deferred.NotificationObserveris unexported, so it still resolves toanyin the published typings.Still to verify on device
iOS: scene reconnect/restore;
didDiscardSceneSessionsdelivered on a later launch;close()denial clearing_closeRequested; suspend/resume ordering across iOS 15–27 with two scenes; background launch (silent push) content deferral; visionOS; embedded hosts; Vite HMR dev boot.Android:
openWindowagainstsingleTask/singleInstancemanifests and OEM recents;putExtraoverload selection for wrapped primitives (resolved by the runtime, not by tsc); per-activityComponentCallbacks2register/unregister across rotation; window-id round trip after process death.Both: CSS system-class consumers — modals, dark mode, rotation — since window-scoped classes no longer live in the global list.
BREAKING CHANGES
NativeWindow.iosWindow→.ios, and its.window→.uiWindowwin.ios?.uiWindow,win.ios?.sceneNativeWindow.androidWindow→.androidwin.android?.activitySceneEventData.windowis now theNativeWindow; the nativeUIWindowmoved touiWindowwindowpayload key always means aNativeWindowgetWindows()is role-filtered, defaulting toapplication+embeddedgetWindows('all')for every registered surfaceexitfires only when the last window finishesexitis unchanged (process termination)suspend/resumein scene mode reflect whole-app statebackground/foregroundfor a single windowdetachedinstead of destroying the windowcloseCSSUtils.getRootViewCssClasses()no longer contains orientation/appearance/direction classesDeprecated but still working:
launch(useready+setWindowContentResolver()),shouldDelayLaunchEvent(now a no-op),Application.orientation()/systemAppearance()/layoutDirection()(delegate toprimaryWindow), and the enumeration getters (getAllWindows/getAllScenes/getWindowScenes/getPrimaryWindow/getPrimaryScene).Un-deprecated: the
activity*/scene*bridges onApplication. They are permanent aggregate APIs — they fire for every window, andargs.windowidentifies which one.