From 8d9189e2d4a134424ebb9670d18cbb0b09c8af16 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 13 Apr 2026 13:17:59 -0300 Subject: [PATCH 1/3] feat: add cross-platform NativeWindow implementation - 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. --- .../core/application/application.android.ts | 109 +++- packages/core/application/application.d.ts | 52 +- packages/core/application/application.ios.ts | 607 +++++++++--------- packages/core/index.d.ts | 1 + packages/core/index.ts | 1 + packages/core/native-window/index.android.ts | 3 + packages/core/native-window/index.d.ts | 2 + packages/core/native-window/index.ios.ts | 3 + .../native-window/native-window-common.ts | 307 +++++++++ .../native-window/native-window-interfaces.ts | 148 +++++ .../native-window/native-window.android.ts | 144 +++++ .../core/native-window/native-window.d.ts | 33 + .../core/native-window/native-window.ios.ts | 216 +++++++ 13 files changed, 1313 insertions(+), 313 deletions(-) create mode 100644 packages/core/native-window/index.android.ts create mode 100644 packages/core/native-window/index.d.ts create mode 100644 packages/core/native-window/index.ios.ts create mode 100644 packages/core/native-window/native-window-common.ts create mode 100644 packages/core/native-window/native-window-interfaces.ts create mode 100644 packages/core/native-window/native-window.android.ts create mode 100644 packages/core/native-window/native-window.d.ts create mode 100644 packages/core/native-window/native-window.ios.ts diff --git a/packages/core/application/application.android.ts b/packages/core/application/application.android.ts index 30e188fd2f..3b58ed0755 100644 --- a/packages/core/application/application.android.ts +++ b/packages/core/application/application.android.ts @@ -8,6 +8,8 @@ import { ApplicationCommon, initializeSdkVersionClass } from './application-comm import type { AndroidActivityBundleEventData, AndroidActivityEventData, ApplicationEventData } from './application-interfaces'; import { Observable } from '../data/observable'; import { Trace } from '../trace'; +import { NativeWindow } from '../native-window/native-window.android'; +import { NativeWindowEvents, WindowEvents } from '../native-window/native-window-interfaces'; import { CommonA11YServiceEnabledObservable, SharedA11YObservable, @@ -78,6 +80,12 @@ function initNativeScriptLifecycleCallbacks() { this.nativescriptActivity = activity; } + // Create and register NativeWindow for this activity + const isPrimary = Application.android._getWindows().length === 0; + const nativeWindowId = NativeWindow.getActivityId(activity); + const nativeWindow = new NativeWindow(activity, nativeWindowId, isPrimary); + Application.android._registerWindow(nativeWindow); + this.notifyActivityCreated(activity, savedInstanceState); if (Application.hasListeners(Application.displayedEvent)) { @@ -105,6 +113,13 @@ function initNativeScriptLifecycleCallbacks() { } } + // Unregister NativeWindow for this activity + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.close); + Application.android._unregisterWindow(nativeWindow); + } + Application.android.notify({ eventName: Application.android.activityDestroyedEvent, object: Application.android, @@ -126,6 +141,11 @@ function initNativeScriptLifecycleCallbacks() { }); } + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.deactivate); + } + Application.android.notify({ eventName: Application.android.activityPausedEvent, object: Application.android, @@ -138,6 +158,11 @@ function initNativeScriptLifecycleCallbacks() { // console.log('NativeScriptLifecycleCallbacks onActivityResumed'); Application.android.setForegroundActivity(activity); + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.activate); + } + // NOTE: setSuspended(false) is called in frame/index.android.ts inside onPostResume // This is done to ensure proper timing for the event to be raised @@ -173,6 +198,11 @@ function initNativeScriptLifecycleCallbacks() { }); } + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.foreground); + } + Application.android.notify({ eventName: Application.android.activityStartedEvent, object: Application.android, @@ -192,6 +222,11 @@ function initNativeScriptLifecycleCallbacks() { }); } + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.background); + } + Application.android.notify({ eventName: Application.android.activityStoppedEvent, object: Application.android, @@ -517,6 +552,78 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp } return []; } + + // --- NativeWindow registry --- + private _windows: NativeWindow[] = []; + + /** + * @internal - Register a NativeWindow created by the lifecycle callbacks. + */ + _registerWindow(nativeWindow: NativeWindow): void { + this._windows.push(nativeWindow); + this.notify({ + eventName: WindowEvents.windowOpen, + object: this, + window: nativeWindow, + }); + } + + /** + * @internal - Unregister a NativeWindow when its activity is destroyed. + */ + _unregisterWindow(nativeWindow: NativeWindow): void { + const idx = this._windows.indexOf(nativeWindow); + if (idx >= 0) { + this._windows.splice(idx, 1); + } + this.notify({ + eventName: WindowEvents.windowClose, + object: this, + window: nativeWindow, + }); + nativeWindow._destroy(); + + // If primary was removed, promote next window + if (nativeWindow.isPrimary && this._windows.length > 0) { + (this._windows[0] as any)._isPrimary = true; + } + } + + /** + * @internal - Get all registered NativeWindows. + */ + _getWindows(): NativeWindow[] { + return this._windows; + } + + /** + * @internal - Get a NativeWindow by its activity. + */ + _getWindowForActivity(activity: androidx.appcompat.app.AppCompatActivity): NativeWindow | undefined { + return this._windows.find((nw) => nw.activity === activity); + } + + /** + * @internal - Get a NativeWindow by its id. + */ + _getWindowById(id: string): NativeWindow | undefined { + return this._windows.find((nw) => nw.id === id); + } + + /** + * Get the primary NativeWindow. + */ + get primaryWindow(): NativeWindow | undefined { + return this._windows.find((nw) => nw.isPrimary); + } + + /** + * Get all active NativeWindows. + */ + getWindows(): NativeWindow[] { + return [...this._windows]; + } + getRootView(): View { const activity = this.foregroundActivity || this.startActivity; if (!activity) { @@ -726,7 +833,7 @@ function updateAccessibilityState(): void { if (!sharedA11YObservable) { return; } - + const accessibilityManager = getAndroidAccessibilityManager(); if (!accessibilityManager) { sharedA11YObservable.set(accessibilityStateEnabledPropName, false); diff --git a/packages/core/application/application.d.ts b/packages/core/application/application.d.ts index a305a88838..f3be8c5639 100644 --- a/packages/core/application/application.d.ts +++ b/packages/core/application/application.d.ts @@ -1,5 +1,6 @@ import { ApplicationCommon } from './application-common'; import { FontScaleCategory } from '../accessibility/font-scale-common'; +import type { NativeWindowCommon } from '../native-window/native-window-common'; export * from './application-common'; export * from './application-interfaces'; @@ -147,6 +148,16 @@ export class AndroidApplication extends ApplicationCommon { on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any): void; on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any): void; on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any): void; + + /** + * Get the primary NativeWindow. + */ + get primaryWindow(): NativeWindowCommon | undefined; + + /** + * Get all active NativeWindows. + */ + getWindows(): NativeWindowCommon[]; } export class iOSApplication extends ApplicationCommon { @@ -230,28 +241,33 @@ export class iOSApplication extends ApplicationCommon { /** * Gets all windows for the application. + * @deprecated Use `getWindows()` instead. */ getAllWindows(): UIWindow[]; /** * Gets all scenes for the application. + * @deprecated Use `getWindows()` instead. */ getAllScenes(): UIScene[]; /** * Gets all window scenes for the application. + * @deprecated Use `getWindows()` instead. */ getWindowScenes(): UIWindowScene[]; /** * Gets the primary window for the application. + * @deprecated Use `primaryWindow?.iosWindow?.window` instead. */ getPrimaryWindow(): UIWindow; /** * Gets the primary scene for the application. + * @deprecated Use `primaryWindow?.iosWindow?.scene` instead. */ - getPrimaryScene(): UIWindowScene; + getPrimaryScene(): UIWindowScene | null; /** * Sets the root view for a specific window. @@ -266,6 +282,40 @@ export class iOSApplication extends ApplicationCommon { */ sceneDelegate: UIWindowSceneDelegate; + /** + * Register a callback to intercept scene configuration. + * + * Called for every new scene session. Return a `UISceneConfiguration` to handle + * the scene yourself (e.g. CarPlay, external display), or return `null`/`undefined` + * to let NativeScript handle it with the default SceneDelegate. + * + * NativeScript only auto-manages `UIWindowSceneSessionRoleApplication` scenes. + * All other scene roles are ignored unless you provide a configuration here. + * + * @example + * ```ts + * Application.ios.onSceneConfiguration = (app, session, options) => { + * if (session.role === CPTemplateApplicationSceneSessionRoleApplication) { + * const config = UISceneConfiguration.configurationWithNameSessionRole('CarPlay', session.role); + * config.delegateClass = MyCarPlaySceneDelegate; + * return config; + * } + * return null; + * }; + * ``` + */ + onSceneConfiguration: ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null; + + /** + * Get the primary NativeWindow. + */ + get primaryWindow(): NativeWindowCommon | undefined; + + /** + * Get all active NativeWindows. + */ + getWindows(): NativeWindowCommon[]; + /** * Flag to be set when the launch event should be delayed until the application has become active. * This is useful when you want to process notifications or data in the background without creating the UI. diff --git a/packages/core/application/application.ios.ts b/packages/core/application/application.ios.ts index df76b1f827..0c6ad4e849 100644 --- a/packages/core/application/application.ios.ts +++ b/packages/core/application/application.ios.ts @@ -11,6 +11,8 @@ import { ApplicationEventData, SceneEventData } from './application-interfaces'; import { Observable } from '../data/observable'; import type { iOSApplication as IiOSApplication } from './application'; import { Trace } from '../trace'; +import { NativeWindow } from '../native-window/native-window.ios'; +import { NativeWindowEvents, WindowEvents } from '../native-window/native-window-interfaces'; import { AccessibilityServiceEnabledPropName, CommonA11YServiceEnabledObservable, @@ -147,8 +149,30 @@ if (supportsScenes()) { * Detected by the Info.plist existence 'UIApplicationSceneManifest'. * If this method is implemented when there is no manifest defined, * the app will boot to a white screen. + * + * Since we configure the delegate dynamically here, UISceneConfigurations + * does NOT need to be present in Info.plist — only UIApplicationSceneManifest is required. + * + * NativeScript only handles UIWindowSceneSessionRoleApplication by default. + * Other scene types (CarPlay, external displays, etc.) are ignored unless + * the user provides an `onSceneConfiguration` callback. */ (Responder.prototype as UIApplicationDelegate).applicationConfigurationForConnectingSceneSessionOptions = function (application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions): UISceneConfiguration { + // Let the user intercept scene configuration for any/all scenes + const userHandler = Application.ios._onSceneConfiguration; + if (userHandler) { + const userConfig = userHandler(application, connectingSceneSession, options); + if (userConfig) { + return userConfig; + } + } + + // Only handle the standard window scene role — skip CarPlay, external displays, etc. + if (connectingSceneSession.role !== UIWindowSceneSessionRoleApplication) { + // Return a bare configuration so iOS doesn't crash, but NativeScript won't manage it + return UISceneConfiguration.configurationWithNameSessionRole('Unmanaged', connectingSceneSession.role); + } + const config = UISceneConfiguration.configurationWithNameSessionRole('Default Configuration', connectingSceneSession.role); config.sceneClass = UIWindowScene as any; config.delegateClass = SceneDelegate; @@ -187,53 +211,82 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { return; } - const isFirstScene = !Application.ios.getPrimaryScene() && !Application.hasLaunched(); + const windowScene = scene as UIWindowScene; + const isFirstScene = Application.ios._getWindows().length === 0 && !Application.hasLaunched(); - this._scene = scene; + this._scene = windowScene; // Create window for this scene - this._window = UIWindow.alloc().initWithWindowScene(scene); + this._window = UIWindow.alloc().initWithWindowScene(windowScene); + + // Set up window background + if (!__VISIONOS__) { + this._window.backgroundColor = SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor; + } - // Store the window scene for this window - Application.ios._setWindowForScene(this._window, scene); + const isPrimary = isFirstScene || !Application.ios.primaryWindow; + const nativeWindowId = NativeWindow.getSceneId(windowScene); - // Set up the window content - Application.ios._setupWindowForScene(this._window, scene); + // Create NativeWindow and register it + const nativeWindow = new NativeWindow(windowScene, this._window, nativeWindowId, isPrimary); + Application.ios._registerWindow(nativeWindow); + + if (isPrimary) { + // For primary, also set the legacy global window reference + setiOSWindow(this._window); + } // Notify that scene will connect Application.ios.notify({ eventName: SceneEvents.sceneWillConnect, object: Application.ios, - scene: scene, + scene: windowScene, window: this._window, connectionOptions: connectionOptions, } as SceneEventData); - if (scene === Application.ios.getPrimaryScene()) { + if (isPrimary) { // primary scene, activate right away this._window.makeKeyAndVisible(); - } else { - // For secondary scenes, emit an event to allow developers to set up custom content for the window - Application.ios.notify({ - eventName: SceneEvents.sceneContentSetup, - object: Application.ios, - scene: scene, - window: this._window, - connectionOptions: connectionOptions, - } as SceneEventData); } // If this is the first scene, trigger app startup if (isFirstScene) { Application.ios._notifySceneAppStarted(); + } else if (isPrimary && Application.ios.hasLaunched()) { + // Primary scene reconnecting after disconnect — restore content + (Application.ios as any).setWindowContent(); } } + sceneDidBecomeActive(scene: UIScene): void { - // This will be handled by the notification observer in iOSApplication - // The notification system will automatically trigger sceneDidActivate + const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.activate); + } + + // If this is the primary scene, trigger traditional app lifecycle + if (nativeWindow?.isPrimary) { + const additionalData = { + ios: UIApplication.sharedApplication, + scene: scene, + }; + Application.ios.setInBackground(false, additionalData); + Application.ios.setSuspended(false, additionalData); + + const rootView = nativeWindow.rootView; + if (rootView && !rootView.isLoaded) { + rootView.callLoaded(); + } + } } sceneWillResignActive(scene: UIScene): void { + const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.deactivate); + } + // Notify that scene will resign active Application.ios.notify({ eventName: SceneEvents.sceneWillResignActive, @@ -243,15 +296,58 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { } sceneWillEnterForeground(scene: UIScene): void { - // This will be handled by the notification observer in iOSApplication + const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.foreground); + } + + Application.ios.notify({ + eventName: SceneEvents.sceneWillEnterForeground, + object: Application.ios, + scene: scene, + } as SceneEventData); } sceneDidEnterBackground(scene: UIScene): void { - // This will be handled by the notification observer in iOSApplication + const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.background); + } + + Application.ios.notify({ + eventName: SceneEvents.sceneDidEnterBackground, + object: Application.ios, + scene: scene, + } as SceneEventData); + + // If this is the primary scene, trigger traditional app lifecycle + if (nativeWindow?.isPrimary) { + const additionalData = { + ios: UIApplication.sharedApplication, + scene: scene, + }; + Application.ios.setInBackground(true, additionalData); + Application.ios.setSuspended(true, additionalData); + + const rootView = nativeWindow.rootView; + if (rootView && rootView.isLoaded) { + rootView.callUnloaded(); + } + } } sceneDidDisconnect(scene: UIScene): void { - // This will be handled by the notification observer in iOSApplication + const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); + if (nativeWindow) { + nativeWindow._notifyEvent(NativeWindowEvents.close); + Application.ios._unregisterWindow(nativeWindow); + } + + Application.ios.notify({ + eventName: SceneEvents.sceneDidDisconnect, + object: Application.ios, + scene: scene, + } as SceneEventData); } } // ensure available globally @@ -263,9 +359,17 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication private _rootView: View; private launchEventCalled = false; private _sceneDelegate: UIWindowSceneDelegate; - private _windowSceneMap = new Map(); - private _primaryScene: UIWindowScene | null = null; - private _openedScenesById = new Map(); + /** + * User-provided callback to intercept scene configuration. + * Called for every new scene session. Return a UISceneConfiguration to handle + * the scene yourself, or return null/undefined to let NativeScript handle it + * (only for UIWindowSceneSessionRoleApplication scenes). + * @internal + */ + _onSceneConfiguration: ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null; + + // NativeWindow registry + private _windows: NativeWindow[] = []; private _notificationObservers: NotificationObserver[] = []; @@ -287,15 +391,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication this.addNotificationObserver(UIApplicationWillTerminateNotification, this.willTerminate.bind(this)); this.addNotificationObserver(UIApplicationDidReceiveMemoryWarningNotification, this.didReceiveMemoryWarning.bind(this)); this.addNotificationObserver(UIApplicationDidChangeStatusBarOrientationNotification, this.didChangeStatusBarOrientation.bind(this)); - - // Add scene lifecycle notification observers only if scenes are supported - if (this.supportsScenes()) { - this.addNotificationObserver('UISceneWillConnectNotification', this.sceneWillConnect.bind(this)); - this.addNotificationObserver('UISceneDidActivateNotification', this.sceneDidActivate.bind(this)); - this.addNotificationObserver('UISceneWillEnterForegroundNotification', this.sceneWillEnterForeground.bind(this)); - this.addNotificationObserver('UISceneDidEnterBackgroundNotification', this.sceneDidEnterBackground.bind(this)); - this.addNotificationObserver('UISceneDidDisconnectNotification', this.sceneDidDisconnect.bind(this)); - } } getRootView(): View { @@ -712,28 +807,36 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication if (!this.launchEventCalled) { this.notifyAppStarted(notification); } - const additionalData = { - ios: UIApplication.sharedApplication, - }; - this.setInBackground(false, additionalData); - this.setSuspended(false, additionalData); - const rootView = this._rootView; - if (rootView && !rootView.isLoaded) { - rootView.callLoaded(); + // Only handle lifecycle here when NOT using scenes + // (scene lifecycle is handled by SceneDelegate methods) + if (!this.supportsScenes()) { + const additionalData = { + ios: UIApplication.sharedApplication, + }; + this.setInBackground(false, additionalData); + this.setSuspended(false, additionalData); + + const rootView = this._rootView; + if (rootView && !rootView.isLoaded) { + rootView.callLoaded(); + } } } private didEnterBackground(notification: NSNotification) { - const additionalData = { - ios: UIApplication.sharedApplication, - }; - this.setInBackground(true, additionalData); - this.setSuspended(true, additionalData); + // Only handle lifecycle here when NOT using scenes + if (!this.supportsScenes()) { + const additionalData = { + ios: UIApplication.sharedApplication, + }; + this.setInBackground(true, additionalData); + this.setSuspended(true, additionalData); - const rootView = this._rootView; - if (rootView && rootView.isLoaded) { - rootView.callUnloaded(); + const rootView = this._rootView; + if (rootView && rootView.isLoaded) { + rootView.callUnloaded(); + } } } @@ -764,167 +867,114 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication this.setOrientation(newOrientation); } - // Scene lifecycle notification handlers - private sceneWillConnect(notification: NSNotification) { - const scene = notification.object as UIWindowScene; - if (!scene || !(scene instanceof UIWindowScene)) { - return; - } - - // Store as primary scene if it's the first one - if (!this._primaryScene) { - this._primaryScene = scene; - } + // --- NativeWindow registry --- - this.notify({ - eventName: SceneEvents.sceneWillConnect, + /** + * @internal - Register a NativeWindow created by the SceneDelegate. + */ + _registerWindow(nativeWindow: NativeWindow): void { + this._windows.push(nativeWindow); + this.notify({ + eventName: WindowEvents.windowOpen, object: this, - scene: scene, - userInfo: notification.userInfo, - } as SceneEventData); + window: nativeWindow, + }); } - private sceneDidActivate(notification: NSNotification) { - const scene = notification.object as UIScene; - this.notify({ - eventName: SceneEvents.sceneDidActivate, + /** + * @internal - Unregister a NativeWindow when its scene disconnects. + */ + _unregisterWindow(nativeWindow: NativeWindow): void { + const idx = this._windows.indexOf(nativeWindow); + if (idx >= 0) { + this._windows.splice(idx, 1); + } + this.notify({ + eventName: WindowEvents.windowClose, object: this, - scene: scene, - } as SceneEventData); - - // If this is the primary scene, trigger traditional app lifecycle - if (scene === this._primaryScene) { - const additionalData = { - ios: UIApplication.sharedApplication, - scene: scene, - }; - this.setInBackground(false, additionalData); - this.setSuspended(false, additionalData); - - if (this._rootView && !this._rootView.isLoaded) { - this._rootView.callLoaded(); + window: nativeWindow, + }); + nativeWindow._destroy(); + + // If primary was removed, promote next window + if (nativeWindow.isPrimary && this._windows.length > 0) { + (this._windows[0] as any)._isPrimary = true; + const promotedWindow = this._windows[0].iosWindow?.window; + if (promotedWindow) { + setiOSWindow(promotedWindow); } } } - private sceneWillEnterForeground(notification: NSNotification) { - const scene = notification.object as UIScene; - this.notify({ - eventName: SceneEvents.sceneWillEnterForeground, - object: this, - scene: scene, - } as SceneEventData); + /** + * @internal - Get all registered NativeWindows. + */ + _getWindows(): NativeWindow[] { + return this._windows; } - private sceneDidEnterBackground(notification: NSNotification) { - const scene = notification.object as UIScene; - this.notify({ - eventName: SceneEvents.sceneDidEnterBackground, - object: this, - scene: scene, - } as SceneEventData); - - // If this is the primary scene, trigger traditional app lifecycle - if (scene === this._primaryScene) { - const additionalData = { - ios: UIApplication.sharedApplication, - scene: scene, - }; - this.setInBackground(true, additionalData); - this.setSuspended(true, additionalData); - - if (this._rootView && this._rootView.isLoaded) { - this._rootView.callUnloaded(); - } - } + /** + * @internal - Get a NativeWindow by its scene. + */ + _getWindowForScene(scene: UIWindowScene): NativeWindow | undefined { + return this._windows.find((nw) => nw.iosWindow?.scene === scene); } - private sceneDidDisconnect(notification: NSNotification) { - const scene = notification.object as UIScene; - this._removeWindowForScene(scene); - - // If primary scene disconnected, clear it - if (scene === this._primaryScene) { - this._primaryScene = null; - } - - if (this._primaryScene) { - if (SDK_VERSION >= 17) { - const request = UISceneSessionActivationRequest.requestWithSession(this._primaryScene.session); + /** + * @internal - Get a NativeWindow by its id. + */ + _getWindowById(id: string): NativeWindow | undefined { + return this._windows.find((nw) => nw.id === id); + } - UIApplication.sharedApplication.activateSceneSessionForRequestErrorHandler(request, (err: NSError) => { - if (err) { - console.log('Failed to activate primary scene:', err.localizedDescription); - } - }); - } else { - UIApplication.sharedApplication.requestSceneSessionActivationUserActivityOptionsErrorHandler(this._primaryScene.session, null, null, (err: NSError) => { - if (err) { - console.log('Failed to activate primary scene (legacy):', err.localizedDescription); - } - }); - } - } + // --- Public NativeWindow API --- - this.notify({ - eventName: SceneEvents.sceneDidDisconnect, - object: this, - scene: scene, - } as SceneEventData); + /** + * Get the primary NativeWindow. + */ + get primaryWindow(): NativeWindow | undefined { + return this._windows.find((nw) => nw.isPrimary); } - // Scene management helper methods - _setWindowForScene(window: UIWindow, scene: UIScene): void { - this._windowSceneMap.set(scene, window); + /** + * Get all active NativeWindows. + */ + getWindows(): NativeWindow[] { + return [...this._windows]; } - _removeWindowForScene(scene: UIScene): void { - this._windowSceneMap.delete(scene); - // also untrack opened scene id - try { - const s: any = scene as any; - if (s && s.session) { - const id = this._getSceneId(s as UIWindowScene); - this._openedScenesById.delete(id); - } - } catch {} + /** + * Register a callback to intercept scene configuration. + * + * Called for every new scene session. Return a `UISceneConfiguration` to handle + * the scene yourself (e.g. CarPlay, external display), or return `null`/`undefined` + * to let NativeScript handle it with the default SceneDelegate. + * + * NativeScript only auto-manages `UIWindowSceneSessionRoleApplication` scenes. + * All other scene roles are ignored unless you provide a configuration here. + * + * @example + * ```ts + * Application.ios.onSceneConfiguration = (app, session, options) => { + * if (session.role === CPTemplateApplicationSceneSessionRoleApplication) { + * const config = UISceneConfiguration.configurationWithNameSessionRole('CarPlay', session.role); + * config.delegateClass = MyCarPlaySceneDelegate; + * return config; + * } + * // Return null to let NativeScript handle the default window scene + * return null; + * }; + * ``` + */ + set onSceneConfiguration(handler: ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null) { + this._onSceneConfiguration = handler; } - _getWindowForScene(scene: UIScene): UIWindow | undefined { - return this._windowSceneMap.get(scene); + get onSceneConfiguration(): ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null { + return this._onSceneConfiguration; } - _setupWindowForScene(window: UIWindow, scene: UIWindowScene): void { - if (!window) { - return; - } - - // track opened scene - try { - const id = this._getSceneId(scene); - this._openedScenesById.set(id, scene); - } catch {} - - // Set up window background - if (!__VISIONOS__) { - window.backgroundColor = SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor; - } - - // If this is the primary scene, set up the main application content - if (scene === this._primaryScene || !this._primaryScene) { - this._primaryScene = scene; - - if (!getiOSWindow()) { - setiOSWindow(window); - } - - // During initial scene startup we must wait for launch to be notified first. - // Some frameworks provide root content from launch handlers. - if (this.hasLaunched()) { - this.setWindowContent(); - } - } - } + // Scene management helper methods (kept for backward compat) get sceneDelegate(): UIWindowSceneDelegate { if (!this._sceneDelegate) { @@ -956,39 +1006,20 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication // iOS 17+ if (SDK_VERSION >= 17) { - // Create a new scene activation request with proper role let request: UISceneSessionActivationRequest; try { - // Use the correct factory method to create request with role - // Based on the type definitions, this is the proper way request = UISceneSessionActivationRequest.requestWithRole(UIWindowSceneSessionRoleApplication); - // Note: may be useful to allow user defined activity type through optional string typed data in future const activity = NSUserActivity.alloc().initWithActivityType(`${NSBundle.mainBundle.bundleIdentifier}.scene`); activity.userInfo = dataSerialize(data); request.userActivity = activity; - // Set proper options with requesting scene const options = UISceneActivationRequestOptions.new(); - - // Note: explore secondary windows spawning other windows - // and if this context needs to change in those cases - const mainWindow = Application.ios.getPrimaryWindow(); - options.requestingScene = mainWindow?.windowScene; - - /** - * Note: This does not work in testing but worth exploring further sometime - * regarding the size/dimensions of opened secondary windows. - * The initial size is ultimately determined by the system - * based on available space and user context. - */ - // Get the size restrictions from the window scene - // const sizeRestrictions = (options.requestingScene as UIWindowScene).sizeRestrictions; - - // // Set your minimum and maximum dimensions - // sizeRestrictions.minimumSize = CGSizeMake(320, 400); - // sizeRestrictions.maximumSize = CGSizeMake(600, 800); + const primary = this.primaryWindow; + if (primary?.iosWindow?.scene) { + options.requestingScene = primary.iosWindow.scene; + } request.options = options; } catch (roleError) { @@ -1000,12 +1031,10 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication if (error) { console.log('Error creating new scene (iOS 17+):', error); - // Log additional debugging info if (error.userInfo) { console.error(`Error userInfo: ${error.userInfo.description}`); } - // Handle specific error types if (error.localizedDescription.includes('role') && error.localizedDescription.includes('nil')) { this.createSceneWithLegacyAPI(data); } else if (error.domain === 'FBSWorkspaceErrorDomain' && error.code === 2) { @@ -1013,22 +1042,13 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication } } }); - } - // iOS 13-16 - Use the legacy requestSceneSessionActivationUserActivityOptionsErrorHandler method - else if (SDK_VERSION >= 13 && SDK_VERSION < 17) { - app.requestSceneSessionActivationUserActivityOptionsErrorHandler( - null, // session - null, // userActivity - null, // options - (error) => { - if (error) { - console.log('Error creating new scene (legacy):', error); - } - }, - ); - } - // Fallback for older iOS versions or unsupported configurations - else { + } else if (SDK_VERSION >= 13 && SDK_VERSION < 17) { + app.requestSceneSessionActivationUserActivityOptionsErrorHandler(null, null, null, (error) => { + if (error) { + console.log('Error creating new scene (legacy):', error); + } + }); + } else { console.log('Neither new nor legacy scene activation methods are available'); } } catch (error) { @@ -1038,76 +1058,72 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication /** * Closes a secondary window/scene. - * Usage examples: - * - Application.ios.closeWindow() // best-effort close of a non-primary scene - * - Application.ios.closeWindow(button) // from a tap handler within the scene - * - Application.ios.closeWindow(window) - * - Application.ios.closeWindow(scene) - * - Application.ios.closeWindow('scene-id') + * Accepts a NativeWindow, View, UIWindow, UIWindowScene, or string id. */ - public closeWindow(target?: View | UIWindow | UIWindowScene | string): void { + public closeWindow(target?: NativeWindow | View | UIWindow | UIWindowScene | string): void { if (!__APPLE__) { return; } try { - const scene = this._resolveScene(target); - if (!scene) { - console.log('closeWindow: No scene resolved for target'); - return; - } + let nativeWindow: NativeWindow | undefined; - // Don't allow closing the primary scene - if (scene === this._primaryScene) { - console.log('closeWindow: Refusing to close the primary scene'); - return; + if (target instanceof NativeWindow) { + nativeWindow = target; + } else { + const scene = this._resolveScene(target); + if (scene) { + nativeWindow = this._getWindowForScene(scene); + } } - const session = scene.session; - if (!session) { - console.log('closeWindow: Scene has no session to destroy'); + if (!nativeWindow) { + console.log('closeWindow: No window resolved for target'); return; } - const app = UIApplication.sharedApplication; - if (app.requestSceneSessionDestructionOptionsErrorHandler) { - app.requestSceneSessionDestructionOptionsErrorHandler(session, null, (error: NSError) => { - if (error) { - console.log('closeWindow: destruction error', error); - } else { - // clean up tracked id - const id = this._getSceneId(scene); - this._openedScenesById.delete(id); - } - }); - } else { - console.info('closeWindow: Scene destruction API not available on this iOS version'); - } + nativeWindow.close(); } catch (err) { console.log('closeWindow: Unexpected error', err); } } + /** + * @deprecated Use `getWindows()` instead. + */ getAllWindows(): UIWindow[] { - return Array.from(this._windowSceneMap.values()); + return this._windows.map((nw) => nw.iosWindow?.window).filter(Boolean) as UIWindow[]; } + /** + * @deprecated Use `getWindows()` instead. + */ getAllScenes(): UIScene[] { - return Array.from(this._windowSceneMap.keys()); + return this._windows.map((nw) => nw.iosWindow?.scene).filter(Boolean) as UIScene[]; } + /** + * @deprecated Use `getWindows()` instead. + */ getWindowScenes(): UIWindowScene[] { return this.getAllScenes().filter((scene) => scene instanceof UIWindowScene) as UIWindowScene[]; } + /** + * @deprecated Use `primaryWindow?.iosWindow?.window` instead. + */ getPrimaryWindow(): UIWindow { - if (this._primaryScene) { - return this._getWindowForScene(this._primaryScene) || getiOSWindow(); + const primary = this.primaryWindow; + if (primary?.iosWindow?.window) { + return primary.iosWindow.window; } return getiOSWindow(); } + /** + * @deprecated Use `primaryWindow?.iosWindow?.scene` instead. + */ getPrimaryScene(): UIWindowScene | null { - return this._primaryScene; + return this.primaryWindow?.iosWindow?.scene || null; } // Scene lifecycle management @@ -1120,7 +1136,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication } isUsingSceneLifecycle(): boolean { - return this.supportsScenes() && this._windowSceneMap.size > 0; + return this.supportsScenes() && this._windows.length > 0; } // Call this to set up scene-based configuration @@ -1129,35 +1145,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication console.warn('Scene-based lifecycle is only supported on iOS 13+ iPad or visionOS with multi-scene enabled apps.'); return; } - - // Additional scene configuration can be added here - // For now, the notification observers are already set up in the constructor - } - - // Stable scene id for lookups - private _getSceneId(scene: UIWindowScene): string { - try { - if (!scene) { - return 'Unknown'; - } - // Prefer session persistentIdentifier when available (stable across lifetime) - const session = scene.session; - const persistentId = session && session.persistentIdentifier; - if (persistentId) { - return `${persistentId}`; - } - // Fallbacks - if (scene.hash != null) { - return `${scene.hash}`; - } - const desc = scene.description; - if (desc) { - return `${desc}`; - } - } catch (err) { - // ignore - } - return 'Unknown'; } // Resolve a UIWindowScene from various input types @@ -1166,12 +1153,10 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication return null; } if (!target) { - // Try to pick a non-primary foreground active scene, else last known scene - const scenes = this.getWindowScenes?.() || []; - const nonPrimary = scenes.filter((s) => s !== this._primaryScene); - return nonPrimary[0] || scenes[0] || null; + // Try to pick a non-primary window's scene + const nonPrimary = this._windows.filter((nw) => !nw.isPrimary); + return nonPrimary[0]?.iosWindow?.scene || this.primaryWindow?.iosWindow?.scene || null; } - // If a View was passed, derive its window.scene if (target && typeof target === 'object') { // UIWindowScene if ((target as UIWindowScene).session && (target as UIWindowScene).activationState !== undefined) { @@ -1190,15 +1175,15 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication } // String id lookup if (typeof target === 'string') { - if (this._openedScenesById.has(target)) { - return this._openedScenesById.get(target); + const found = this._getWindowById(target); + if (found) { + return found.iosWindow?.scene || null; } - // Try matching by persistentIdentifier or hash among known scenes - const scenes = this.getWindowScenes?.() || []; - for (const s of scenes) { - const sid = this._getSceneId(s); - if (sid === target) { - return s; + // Try matching among known scenes + for (const nw of this._windows) { + const scene = nw.iosWindow?.scene; + if (scene && NativeWindow.getSceneId(scene) === target) { + return scene; } } } diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index adc95121cb..912419e1fb 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -10,6 +10,7 @@ export type { NativeScriptConfig } from './config'; export * from './application'; export { androidRegisterBroadcastReceiver, androidUnregisterBroadcastReceiver, androidRegisteredReceivers, iosAddNotificationObserver, iosRemoveNotificationObserver, iosNotificationObservers } from './application/helpers'; export { getNativeApp, setNativeApp } from './application/helpers-common'; +export * from './native-window'; export * as ApplicationSettings from './application-settings'; export namespace AccessibilityEvents { export const accessibilityBlurEvent: 'accessibilityBlur'; diff --git a/packages/core/index.ts b/packages/core/index.ts index 4e12c9fe93..f7fe4efefc 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -4,6 +4,7 @@ import './globals'; export * from './application'; export { getNativeApp, setNativeApp } from './application/helpers-common'; +export * from './native-window'; export * as ApplicationSettings from './application-settings'; import * as Accessibility from './accessibility'; export namespace AccessibilityEvents { diff --git a/packages/core/native-window/index.android.ts b/packages/core/native-window/index.android.ts new file mode 100644 index 0000000000..83ccbf211d --- /dev/null +++ b/packages/core/native-window/index.android.ts @@ -0,0 +1,3 @@ +export * from './native-window-interfaces'; +export * from './native-window-common'; +export * from './native-window'; diff --git a/packages/core/native-window/index.d.ts b/packages/core/native-window/index.d.ts new file mode 100644 index 0000000000..96264fa3ca --- /dev/null +++ b/packages/core/native-window/index.d.ts @@ -0,0 +1,2 @@ +export * from './native-window-interfaces'; +export * from './native-window-common'; diff --git a/packages/core/native-window/index.ios.ts b/packages/core/native-window/index.ios.ts new file mode 100644 index 0000000000..83ccbf211d --- /dev/null +++ b/packages/core/native-window/index.ios.ts @@ -0,0 +1,3 @@ +export * from './native-window-interfaces'; +export * from './native-window-common'; +export * from './native-window'; diff --git a/packages/core/native-window/native-window-common.ts b/packages/core/native-window/native-window-common.ts new file mode 100644 index 0000000000..efb9f4924e --- /dev/null +++ b/packages/core/native-window/native-window-common.ts @@ -0,0 +1,307 @@ +import { Observable } from '../data/observable'; +import { CoreTypes } from '../core-types'; +import { CSSUtils } from '../css/system-classes'; +import { Device } from '../platform'; +import { Trace } from '../trace'; +import { Builder } from '../ui/builder'; +import type { View } from '../ui/core/view'; +import type { Frame } from '../ui/frame'; +import type { NavigationEntry } from '../ui/frame/frame-interfaces'; +import type { StyleScope } from '../ui/styling/style-scope'; +import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common'; +import { SDK_VERSION } from '../utils/constants'; +import { initializeSdkVersionClass } from '../application/application-common'; +import type { INativeWindow, NativeWindowEventData, NativeWindowEventName } from './native-window-interfaces'; +import { NativeWindowEvents } from './native-window-interfaces'; + +// prettier-ignore +const ORIENTATION_CSS_CLASSES = [ + `${CSSUtils.CLASS_PREFIX}${CoreTypes.DeviceOrientation.portrait}`, + `${CSSUtils.CLASS_PREFIX}${CoreTypes.DeviceOrientation.landscape}`, + `${CSSUtils.CLASS_PREFIX}${CoreTypes.DeviceOrientation.unknown}`, +]; + +// prettier-ignore +const SYSTEM_APPEARANCE_CSS_CLASSES = [ + `${CSSUtils.CLASS_PREFIX}${CoreTypes.SystemAppearance.light}`, + `${CSSUtils.CLASS_PREFIX}${CoreTypes.SystemAppearance.dark}`, +]; + +// prettier-ignore +const LAYOUT_DIRECTION_CSS_CLASSES = [ + `${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.ltr}`, + `${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.rtl}`, +]; + +let _windowIdCounter = 0; + +/** + * Cross-platform NativeWindow base class. + * + * Wraps a platform window surface (iOS UIWindowScene+UIWindow, Android Activity) + * and manages per-window root view lifecycle, CSS classes, and events. + * + * Platform-specific subclasses implement the abstract methods. + */ +export abstract class NativeWindowCommon extends Observable implements INativeWindow { + private _id: string; + private _isPrimary: boolean; + protected _rootView: View; + protected _orientation: 'portrait' | 'landscape' | 'unknown'; + protected _systemAppearance: 'dark' | 'light' | null; + protected _layoutDirection: CoreTypes.LayoutDirectionType | null; + + constructor(id?: string, isPrimary = false) { + super(); + this._id = id || `window-${++_windowIdCounter}`; + this._isPrimary = isPrimary; + } + + get id(): string { + return this._id; + } + + get isPrimary(): boolean { + return this._isPrimary; + } + + /** + * @internal - used by the Application to promote a window to primary. + */ + _setIsPrimary(value: boolean): void { + this._isPrimary = value; + } + + get rootView(): View { + return this._rootView; + } + + /** + * Set the content of this window. + * Accepts a View, a NavigationEntry, or a module name string. + */ + setContent(content: View | NavigationEntry | string): void { + let view: View; + + if (typeof content === 'string') { + view = Builder.createViewFromEntry({ moduleName: content }); + } else if (content && typeof content === 'object') { + if ((content as NavigationEntry).moduleName || (content as NavigationEntry).create) { + view = Builder.createViewFromEntry(content as NavigationEntry); + } else { + view = content as View; + } + } + + if (!view) { + throw new Error('NativeWindow.setContent: Invalid content provided.'); + } + + const previousRootView = this._rootView; + if (previousRootView) { + previousRootView._onRootViewReset(); + } + + this._rootView = view; + this._applyRootViewSettings(view); + this._setNativeContent(view); + + this._notifyEvent(NativeWindowEvents.contentLoaded); + } + + /** + * Platform-specific: apply the view to the native window surface. + */ + protected abstract _setNativeContent(view: View): void; + + /** + * Close this window. + */ + abstract close(): void; + + /** + * Get the current orientation of this window. + */ + orientation(): 'portrait' | 'landscape' | 'unknown' { + return (this._orientation ??= this._getOrientation()); + } + + /** + * Get the current system appearance of this window. + */ + systemAppearance(): 'light' | 'dark' | null { + return (this._systemAppearance ??= this._getSystemAppearance()); + } + + /** + * Get the current layout direction of this window. + */ + layoutDirection(): CoreTypes.LayoutDirectionType | null { + return (this._layoutDirection ??= this._getLayoutDirection()); + } + + get iosWindow(): INativeWindow['iosWindow'] { + return undefined; + } + + get androidWindow(): INativeWindow['androidWindow'] { + return undefined; + } + + // Platform-specific abstract getters + protected abstract _getOrientation(): 'portrait' | 'landscape' | 'unknown'; + protected abstract _getSystemAppearance(): 'light' | 'dark' | null; + protected abstract _getLayoutDirection(): CoreTypes.LayoutDirectionType | null; + + // --- Root view CSS class management --- + + /** + * Applies platform, orientation, appearance, and layout direction CSS classes + * to the root view. + */ + protected _applyRootViewSettings(rootView: View): void { + rootView._setupAsRootView({}); + this._setRootViewCSSClasses(rootView); + readyInitAccessibilityCssHelper(); + readyInitFontScale(); + initializeSdkVersionClass(rootView); + } + + private _setRootViewCSSClasses(rootView: View): void { + const platform = Device.os.toLowerCase(); + const deviceType = Device.deviceType.toLowerCase(); + const orientationValue = this.orientation(); + const appearanceValue = this.systemAppearance(); + const directionValue = this.layoutDirection(); + + if (platform) { + CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${platform}`); + } + + if (deviceType) { + CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${deviceType}`); + } + + if (orientationValue) { + CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${orientationValue}`); + } + + if (appearanceValue) { + CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${appearanceValue}`); + } + + if (directionValue) { + CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${directionValue}`); + } + + rootView.cssClasses.add(CSSUtils.ROOT_VIEW_CSS_CLASS); + const rootViewCssClasses = CSSUtils.getSystemCssClasses(); + rootViewCssClasses.forEach((c) => rootView.cssClasses.add(c)); + + this._increaseStyleScopeVersion(rootView); + rootView._onCssStateChange(); + + if (Trace.isEnabled()) { + const rootCssClasses = Array.from(rootView.cssClasses); + Trace.write(`NativeWindow [${this._id}] Setting root css classes: ${rootCssClasses.join(' ')}`, Trace.categories.Style); + } + } + + // --- Orientation / Appearance / Direction change handling --- + + /** + * @internal – called by platform when orientation changes for this window. + */ + _setOrientation(value: 'portrait' | 'landscape' | 'unknown'): void { + if (this._orientation === value) { + return; + } + this._orientation = value; + if (this._rootView) { + const cssClass = `${CSSUtils.CLASS_PREFIX}${value}`; + this._applyCssClass(this._rootView, ORIENTATION_CSS_CLASSES, cssClass); + } + } + + /** + * @internal – called by platform when system appearance changes for this window. + */ + _setSystemAppearance(value: 'dark' | 'light'): void { + if (this._systemAppearance === value) { + return; + } + this._systemAppearance = value; + if (this._rootView) { + const cssClass = `${CSSUtils.CLASS_PREFIX}${value}`; + this._applyCssClass(this._rootView, SYSTEM_APPEARANCE_CSS_CLASSES, cssClass); + } + } + + /** + * @internal – called by platform when layout direction changes for this window. + */ + _setLayoutDirection(value: CoreTypes.LayoutDirectionType): void { + if (this._layoutDirection === value) { + return; + } + this._layoutDirection = value; + if (this._rootView) { + const cssClass = `${CSSUtils.CLASS_PREFIX}${value}`; + this._applyCssClass(this._rootView, LAYOUT_DIRECTION_CSS_CLASSES, cssClass); + } + } + + // --- Internal helpers --- + + private _applyCssClass(rootView: View, cssClasses: string[], newCssClass: string): void { + if (!rootView.cssClasses.has(newCssClass)) { + cssClasses.forEach((cssClass) => { + CSSUtils.removeSystemCssClass(cssClass); + rootView.cssClasses.delete(cssClass); + }); + CSSUtils.pushToSystemCssClasses(newCssClass); + rootView.cssClasses.add(newCssClass); + this._increaseStyleScopeVersion(rootView); + rootView._onCssStateChange(); + } + + // Apply to modal views + const rootModalViews = >rootView._getRootModalViews(); + rootModalViews.forEach((modalView) => { + if (!modalView.cssClasses.has(newCssClass)) { + cssClasses.forEach((cssClass) => modalView.cssClasses.delete(cssClass)); + modalView.cssClasses.add(newCssClass); + modalView._onCssStateChange(); + } + }); + } + + private _increaseStyleScopeVersion(rootView: View): void { + const styleScope: StyleScope = rootView._styleScope ?? (rootView as unknown as Frame)?.currentPage?._styleScope; + if (styleScope) { + styleScope._increaseApplicationCssSelectorVersion(); + } + } + + /** + * @internal – emit a NativeWindow lifecycle event. + */ + _notifyEvent(eventName: NativeWindowEventName): void { + this.notify({ + eventName, + window: this, + object: this, + }); + } + + /** + * @internal – called when the window is being torn down. + */ + _destroy(): void { + this._notifyEvent(NativeWindowEvents.close); + if (this._rootView) { + this._rootView._onRootViewReset(); + this._rootView = null; + } + } +} diff --git a/packages/core/native-window/native-window-interfaces.ts b/packages/core/native-window/native-window-interfaces.ts new file mode 100644 index 0000000000..cd8ce89d74 --- /dev/null +++ b/packages/core/native-window/native-window-interfaces.ts @@ -0,0 +1,148 @@ +import type { EventData } from '../data/observable'; +import type { View } from '../ui/core/view'; +import type { NavigationEntry } from '../ui/frame/frame-interfaces'; +import type { CoreTypes } from '../core-types'; + +/** + * Events emitted by a NativeWindow instance. + */ +export const NativeWindowEvents = { + /** Fired when the window becomes the active/focused window. */ + activate: 'activate', + /** Fired when the window loses focus. */ + deactivate: 'deactivate', + /** Fired when the window enters the background. */ + background: 'background', + /** Fired when the window enters the foreground. */ + foreground: 'foreground', + /** Fired when the window is being closed/destroyed. */ + close: 'close', + /** Fired after the window content has been displayed for the first time. */ + displayed: 'displayed', + /** Fired when the root view content is set or changed. */ + contentLoaded: 'contentLoaded', +} as const; + +export type NativeWindowEventName = (typeof NativeWindowEvents)[keyof typeof NativeWindowEvents]; + +/** + * Application-level events related to window management. + */ +export const WindowEvents = { + /** Fired on Application when a new NativeWindow is created. */ + windowOpen: 'windowOpen', + /** Fired on Application when a NativeWindow is closed/destroyed. */ + windowClose: 'windowClose', +} as const; + +/** + * Base event data for NativeWindow events. + */ +export interface NativeWindowEventData extends EventData { + /** The NativeWindow that emitted the event. */ + window: INativeWindow; +} + +/** + * Event data fired on Application when a window opens. + */ +export interface WindowOpenEventData extends EventData { + /** The NativeWindow that was opened. */ + window: INativeWindow; +} + +/** + * Event data fired on Application when a window closes. + */ +export interface WindowCloseEventData extends EventData { + /** The NativeWindow that was closed. */ + window: INativeWindow; +} + +/** + * Cross-platform NativeWindow interface. + * + * A NativeWindow represents a platform window surface: + * - iOS: UIWindowScene + UIWindow + * - Android: Activity + * + * Each NativeWindow manages its own root view, lifecycle events, + * and platform-specific accessors. + */ +export interface INativeWindow { + /** + * A stable identifier for this window. + * On iOS: derived from the UISceneSession persistentIdentifier. + * On Android: derived from the Activity hashCode. + */ + readonly id: string; + + /** + * Whether this is the primary (main) window. + * The first window created is typically the primary window. + * Application-level lifecycle events are bridged from the primary window. + */ + readonly isPrimary: boolean; + + /** + * The current root view of this window. + */ + readonly rootView: View; + + /** + * Set the content of this window. + * @param content A View instance, a NavigationEntry, or a module name string. + */ + setContent(content: View | NavigationEntry | string): void; + + /** + * Close this window. + * The primary window cannot be closed. + * + * iOS: requests scene session destruction. + * Android: finishes the activity. + */ + close(): void; + + /** + * The current orientation of this window. + */ + orientation(): 'portrait' | 'landscape' | 'unknown'; + + /** + * The current system appearance (light/dark) for this window. + */ + systemAppearance(): 'light' | 'dark' | null; + + /** + * The current layout direction for this window. + */ + layoutDirection(): CoreTypes.LayoutDirectionType | null; + + /** + * iOS-specific accessors. Only available when running on iOS. + */ + readonly iosWindow?: { + readonly scene: UIWindowScene; + readonly window: UIWindow; + }; + + /** + * Android-specific accessors. Only available when running on Android. + */ + readonly androidWindow?: { + readonly activity: androidx.appcompat.app.AppCompatActivity; + }; +} + +/** + * Options for opening a new window. + */ +export interface WindowOpenOptions { + /** + * Data to pass to the new window. + * On iOS: serialized into NSUserActivity.userInfo. + * On Android: added as intent extras. + */ + data?: Record; +} diff --git a/packages/core/native-window/native-window.android.ts b/packages/core/native-window/native-window.android.ts new file mode 100644 index 0000000000..8676a843bd --- /dev/null +++ b/packages/core/native-window/native-window.android.ts @@ -0,0 +1,144 @@ +import type { View } from '../ui/core/view'; +import { CoreTypes } from '../core-types'; +import { SDK_VERSION } from '../utils/constants'; +import { AndroidActivityCallbacks, NavigationEntry } from '../ui/frame/frame-common'; +import { NativeWindowCommon } from './native-window-common'; +import type { INativeWindow } from './native-window-interfaces'; + +/** + * Android implementation of NativeWindow. + * Wraps an AppCompatActivity. + */ +export class NativeWindow extends NativeWindowCommon { + private _activity: WeakRef; + + constructor(activity: androidx.appcompat.app.AppCompatActivity, id: string, isPrimary = false) { + super(id, isPrimary); + this._activity = new WeakRef(activity); + } + + /** + * The wrapped Android Activity (may be GC'd). + */ + get activity(): androidx.appcompat.app.AppCompatActivity | undefined { + return this._activity?.deref(); + } + + get androidWindow(): INativeWindow['androidWindow'] { + const activity = this.activity; + if (!activity) { + return undefined; + } + return { activity }; + } + + /** + * Platform-specific: apply the view as root content of this Activity. + */ + protected _setNativeContent(view: View): void { + const activity = this.activity; + if (!activity) { + throw new Error('NativeWindow: Activity is no longer available.'); + } + + const callbacks: AndroidActivityCallbacks = (activity as any)['_callbacks']; + if (!callbacks) { + throw new Error('NativeWindow: Cannot find activity callbacks.'); + } + callbacks.resetActivityContent(activity); + } + + /** + * Close this window by finishing the activity. + */ + close(): void { + if (this.isPrimary) { + console.log('NativeWindow: Cannot close the primary window.'); + return; + } + + const activity = this.activity; + if (activity) { + activity.finish(); + } + } + + // --- Platform getters --- + + protected _getOrientation(): 'portrait' | 'landscape' | 'unknown' { + const activity = this.activity; + if (!activity) { + return 'unknown'; + } + const configuration = activity.getResources().getConfiguration(); + return this._getOrientationValue(configuration); + } + + protected _getSystemAppearance(): 'light' | 'dark' | null { + const activity = this.activity; + if (!activity) { + return null; + } + const configuration = activity.getResources().getConfiguration(); + return this._getSystemAppearanceValue(configuration); + } + + protected _getLayoutDirection(): CoreTypes.LayoutDirectionType | null { + const activity = this.activity; + if (!activity) { + return null; + } + const configuration = activity.getResources().getConfiguration(); + return this._getLayoutDirectionValue(configuration); + } + + // --- Value converters --- + + _getOrientationValue(configuration: android.content.res.Configuration): 'portrait' | 'landscape' | 'unknown' { + switch (configuration.orientation) { + case android.content.res.Configuration.ORIENTATION_LANDSCAPE: + return 'landscape'; + case android.content.res.Configuration.ORIENTATION_PORTRAIT: + return 'portrait'; + default: + return 'unknown'; + } + } + + _getSystemAppearanceValue(configuration: android.content.res.Configuration): 'dark' | 'light' { + const mode = configuration.uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK; + switch (mode) { + case android.content.res.Configuration.UI_MODE_NIGHT_YES: + return 'dark'; + case android.content.res.Configuration.UI_MODE_NIGHT_NO: + case android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED: + default: + return 'light'; + } + } + + _getLayoutDirectionValue(configuration: android.content.res.Configuration): CoreTypes.LayoutDirectionType { + switch (configuration.getLayoutDirection()) { + case android.view.View.LAYOUT_DIRECTION_RTL: + return CoreTypes.LayoutDirection.rtl; + case android.view.View.LAYOUT_DIRECTION_LTR: + default: + return CoreTypes.LayoutDirection.ltr; + } + } + + /** + * @internal + */ + _destroy(): void { + super._destroy(); + this._activity = null; + } + + /** + * Gets a stable identifier from an Activity. + */ + static getActivityId(activity: androidx.appcompat.app.AppCompatActivity): string { + return `activity-${activity.hashCode()}`; + } +} diff --git a/packages/core/native-window/native-window.d.ts b/packages/core/native-window/native-window.d.ts new file mode 100644 index 0000000000..a089657564 --- /dev/null +++ b/packages/core/native-window/native-window.d.ts @@ -0,0 +1,33 @@ +import { NativeWindowCommon } from './native-window-common'; +import type { INativeWindow, NativeWindowEventData } from './native-window-interfaces'; + +export { NativeWindowCommon } from './native-window-common'; +export * from './native-window-interfaces'; + +/** + * Cross-platform NativeWindow class. + * + * On iOS: wraps a UIWindowScene + UIWindow. + * On Android: wraps an AppCompatActivity. + * + * Use `Application.primaryWindow` to get the main window, + * or `Application.getWindows()` to get all active windows. + */ +export class NativeWindow extends NativeWindowCommon implements INativeWindow { + readonly iosWindow: + | { + readonly scene: UIWindowScene; + readonly window: UIWindow; + } + | undefined; + + readonly androidWindow: + | { + readonly activity: androidx.appcompat.app.AppCompatActivity; + } + | undefined; + // Event methods (inherited from Observable) + on(eventName: string, callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + once(eventName: string, callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + off(eventName: string, callback?: (data: NativeWindowEventData) => void, thisArg?: any): void; +} diff --git a/packages/core/native-window/native-window.ios.ts b/packages/core/native-window/native-window.ios.ts new file mode 100644 index 0000000000..f97709729a --- /dev/null +++ b/packages/core/native-window/native-window.ios.ts @@ -0,0 +1,216 @@ +import type { View } from '../ui/core/view'; +import { IOSHelper } from '../ui/core/view/view-helper'; +import { SDK_VERSION } from '../utils/constants'; +import { CoreTypes } from '../core-types'; +import { NativeWindowCommon } from './native-window-common'; +import { NativeWindowEvents } from './native-window-interfaces'; +import type { INativeWindow } from './native-window-interfaces'; + +/** + * iOS implementation of NativeWindow. + * Wraps a UIWindowScene + UIWindow pair. + */ +export class NativeWindow extends NativeWindowCommon { + private _scene: UIWindowScene; + private _window: UIWindow; + + constructor(scene: UIWindowScene, window: UIWindow, id: string, isPrimary = false) { + super(id, isPrimary); + this._scene = scene; + this._window = window; + } + + get iosWindow(): INativeWindow['iosWindow'] { + return { + scene: this._scene, + window: this._window, + }; + } + + /** + * Platform-specific: set the view as root content of this UIWindow. + */ + protected _setNativeContent(view: View): void { + const controller = this._getViewController(view); + this._setViewControllerView(view); + + const haveController = this._window.rootViewController !== null; + this._window.rootViewController = controller; + + if (!haveController) { + this._window.makeKeyAndVisible(); + } + + // Listen for trait collection changes per-window + view.on(IOSHelper.traitCollectionColorAppearanceChangedEvent, () => { + const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; + this._setSystemAppearance(this._getSystemAppearanceValue(userInterfaceStyle)); + }); + + view.on(IOSHelper.traitCollectionLayoutDirectionChangedEvent, () => { + const layoutDirection = controller.traitCollection.layoutDirection; + this._setLayoutDirection(this._getLayoutDirectionValue(layoutDirection)); + }); + } + + /** + * Close this window/scene. + */ + close(): void { + if (this.isPrimary) { + console.log('NativeWindow: Cannot close the primary window.'); + return; + } + + const session = this._scene?.session; + if (!session) { + console.log('NativeWindow: Scene has no session to destroy.'); + return; + } + + const app = UIApplication.sharedApplication; + if (app.requestSceneSessionDestructionOptionsErrorHandler) { + app.requestSceneSessionDestructionOptionsErrorHandler(session, null, (error: NSError) => { + if (error) { + console.log('NativeWindow: Error destroying scene session:', error.localizedDescription); + } + }); + } else { + console.log('NativeWindow: Scene destruction API not available on this iOS version.'); + } + } + + // --- Platform getters --- + + protected _getOrientation(): 'portrait' | 'landscape' | 'unknown' { + if (__VISIONOS__) { + return this._getOrientationValue(NativeScriptEmbedder.sharedInstance().windowScene?.interfaceOrientation); + } + if (this._scene) { + return this._getOrientationValue(this._scene.interfaceOrientation); + } + return this._getOrientationValue(UIApplication.sharedApplication.statusBarOrientation); + } + + protected _getSystemAppearance(): 'light' | 'dark' | null { + if (!__VISIONOS__ && SDK_VERSION <= 11) { + return null; + } + const rootVC = this._window?.rootViewController; + if (!rootVC) { + return null; + } + return this._getSystemAppearanceValue(rootVC.traitCollection.userInterfaceStyle); + } + + protected _getLayoutDirection(): CoreTypes.LayoutDirectionType | null { + const rootVC = this._window?.rootViewController; + if (!rootVC) { + return null; + } + return this._getLayoutDirectionValue(rootVC.traitCollection.layoutDirection); + } + + // --- Value converters --- + + private _getOrientationValue(orientation: number): 'portrait' | 'landscape' | 'unknown' { + switch (orientation) { + case UIInterfaceOrientation.LandscapeRight: + case UIInterfaceOrientation.LandscapeLeft: + return 'landscape'; + case UIInterfaceOrientation.PortraitUpsideDown: + case UIInterfaceOrientation.Portrait: + return 'portrait'; + case UIInterfaceOrientation.Unknown: + default: + return 'unknown'; + } + } + + _getSystemAppearanceValue(userInterfaceStyle: number): 'dark' | 'light' { + switch (userInterfaceStyle) { + case UIUserInterfaceStyle.Dark: + return 'dark'; + case UIUserInterfaceStyle.Light: + case UIUserInterfaceStyle.Unspecified: + default: + return 'light'; + } + } + + _getLayoutDirectionValue(layoutDirection: number): CoreTypes.LayoutDirectionType { + switch (layoutDirection) { + case UITraitEnvironmentLayoutDirection.RightToLeft: + return CoreTypes.LayoutDirection.rtl; + case UITraitEnvironmentLayoutDirection.LeftToRight: + default: + return CoreTypes.LayoutDirection.ltr; + } + } + + // --- ViewController helpers --- + + private _getViewController(rootView: View): UIViewController { + let viewController: UIViewController = rootView.viewController || rootView.ios; + + if (!(viewController instanceof UIViewController)) { + viewController = IOSHelper.UILayoutViewController.initWithOwner(new WeakRef(rootView)) as UIViewController; + rootView.viewController = viewController; + } + + return viewController; + } + + private _setViewControllerView(view: View): void { + const viewController: UIViewController = view.viewController || view.ios; + const nativeView = view.ios || view.nativeViewProtected; + + if (!nativeView || !viewController) { + throw new Error('Root should be either UIViewController or UIView'); + } + + if (viewController instanceof IOSHelper.UILayoutViewController) { + viewController.view.addSubview(nativeView); + } + } + + /** + * @internal + */ + _destroy(): void { + // Remove trait collection listeners from root view before destroying + if (this._rootView) { + this._rootView.off(IOSHelper.traitCollectionColorAppearanceChangedEvent); + this._rootView.off(IOSHelper.traitCollectionLayoutDirectionChangedEvent); + } + super._destroy(); + this._scene = null; + this._window = null; + } + + /** + * Gets the stable scene identifier. + */ + static getSceneId(scene: UIWindowScene): string { + try { + if (!scene) { + return 'unknown'; + } + const session = scene.session; + const persistentId = session?.persistentIdentifier; + if (persistentId) { + return `${persistentId}`; + } + if (scene.hash != null) { + return `${scene.hash}`; + } + const desc = scene.description; + if (desc) { + return `${desc}`; + } + } catch { + // ignore + } + return 'unknown'; + } +} From 995ea5f11a9e798716a9ed3053b28a9a8f966e65 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 13 Apr 2026 15:56:00 -0300 Subject: [PATCH 2/3] refactor: Update NativeWindow implementation and lifecycle event handling - 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. --- apps/toolbox/src/pages/multiple-scenes.ts | 144 ++++++++++++------ .../core/application/application-common.ts | 14 ++ .../core/application/application.android.ts | 80 ++++++++-- packages/core/application/application.d.ts | 135 ++++++++++++++-- packages/core/application/application.ios.ts | 86 ++++++++--- packages/core/native-window/index.android.ts | 3 - packages/core/native-window/index.ios.ts | 3 - .../native-window/{index.d.ts => index.ts} | 0 .../native-window/native-window-common.ts | 42 ++++- .../native-window/native-window-interfaces.ts | 124 +++++---------- .../native-window/native-window.android.ts | 7 +- .../core/native-window/native-window.d.ts | 33 ---- .../core/native-window/native-window.ios.ts | 7 +- packages/core/ui/frame/index.android.ts | 102 +++++++++++-- 14 files changed, 536 insertions(+), 244 deletions(-) delete mode 100644 packages/core/native-window/index.android.ts delete mode 100644 packages/core/native-window/index.ios.ts rename packages/core/native-window/{index.d.ts => index.ts} (100%) delete mode 100644 packages/core/native-window/native-window.d.ts diff --git a/apps/toolbox/src/pages/multiple-scenes.ts b/apps/toolbox/src/pages/multiple-scenes.ts index a35c1d2b20..7c592df2d9 100644 --- a/apps/toolbox/src/pages/multiple-scenes.ts +++ b/apps/toolbox/src/pages/multiple-scenes.ts @@ -1,4 +1,4 @@ -import { Observable, EventData, Page, Application, Frame, StackLayout, Label, Button, Dialogs, View, Color, SceneEvents, SceneEventData, Utils } from '@nativescript/core'; +import { Observable, EventData, Page, Application, StackLayout, Label, Button, Dialogs, View, Color, NativeWindowEvents, SceneEventData, Utils, WindowEvents, WindowOpenEventData, WindowCloseEventData, NativeWindow } from '@nativescript/core'; let page: Page; let viewModel: MultipleScenesModel; @@ -9,12 +9,21 @@ export function navigatingTo(args: EventData) { page.bindingContext = viewModel; } +export function navigatingFrom(args: EventData) { + if (viewModel) { + viewModel.destroy(); + viewModel = undefined; + } +} + export class MultipleScenesModel extends Observable { private _sceneCount = 0; private _isMultiSceneSupported = false; - private _currentScenes: any[] = []; private _currentWindows: any[] = []; private _sceneEvents: string[] = []; + private _windowOpenHandler: (args: WindowOpenEventData) => void; + private _windowCloseHandler: (args: WindowCloseEventData) => void; + private _sceneEventHandlers: Map void> = new Map(); constructor() { super(); @@ -32,10 +41,6 @@ export class MultipleScenesModel extends Observable { return this._isMultiSceneSupported; } - get currentScenes(): any[] { - return this._currentScenes; - } - get currentWindows(): any[] { return this._currentWindows; } @@ -105,39 +110,84 @@ export class MultipleScenesModel extends Observable { private setupSceneEventListeners() { if (!__APPLE__) return; - // Listen to all scene lifecycle events - Application.on(SceneEvents.sceneWillConnect, (args: SceneEventData) => { - this.addSceneEvent(`Scene Will Connect: ${this.getSceneDescription(args.scene)}`); + // Listen for window open/close on Application + this._windowOpenHandler = (args: WindowOpenEventData) => { + const nativeWindow = args.window; + if (!nativeWindow) return; + this.addSceneEvent(`Window opened: ${nativeWindow.id}`); + this.registerNativeWindowListeners(nativeWindow); this.updateSceneInfo(); - }); - - Application.on(SceneEvents.sceneDidActivate, (args: SceneEventData) => { - this.addSceneEvent(`Scene Did Activate: ${this.getSceneDescription(args.scene)}`); + }; + this._windowCloseHandler = (args: WindowCloseEventData) => { + const nativeWindow = args.window; + if (!nativeWindow) return; + this.addSceneEvent(`Window closed: ${nativeWindow.id}`); this.updateSceneInfo(); - }); + }; + Application.ios.on(WindowEvents.windowOpen, this._windowOpenHandler); + Application.ios.on(WindowEvents.windowClose, this._windowCloseHandler); - Application.on(SceneEvents.sceneWillResignActive, (args: SceneEventData) => { - this.addSceneEvent(`Scene Will Resign Active: ${this.getSceneDescription(args.scene)}`); - }); + // Register listeners on existing windows + for (const nativeWindow of Application.ios.getWindows()) { + this.registerNativeWindowListeners(nativeWindow); + } + } - Application.on(SceneEvents.sceneWillEnterForeground, (args: SceneEventData) => { - this.addSceneEvent(`Scene Will Enter Foreground: ${this.getSceneDescription(args.scene)}`); - }); + private registerNativeWindowListeners(nativeWindow: NativeWindow) { + const events = [ + { name: NativeWindowEvents.sceneWillConnect, label: 'Scene Will Connect' }, + { name: NativeWindowEvents.sceneDidActivate, label: 'Scene Did Activate' }, + { name: NativeWindowEvents.sceneWillResignActive, label: 'Scene Will Resign Active' }, + { name: NativeWindowEvents.sceneWillEnterForeground, label: 'Scene Will Enter Foreground' }, + { name: NativeWindowEvents.sceneDidEnterBackground, label: 'Scene Did Enter Background' }, + { name: NativeWindowEvents.sceneDidDisconnect, label: 'Scene Did Disconnect' }, + ]; + + for (const event of events) { + const handler = (args: SceneEventData) => { + this.addSceneEvent(`${event.label}: Window ${nativeWindow.id}`); + this.updateSceneInfo(); + + // Set up content for new scenes when they connect + if (event.name === NativeWindowEvents.sceneWillConnect) { + this.setupSceneContent(nativeWindow, args); + } + }; + const handlerKey = `${nativeWindow.id}:${event.name}`; + this._sceneEventHandlers.set(handlerKey, handler); + nativeWindow.on(event.name, handler as any); + } + } - Application.on(SceneEvents.sceneDidEnterBackground, (args: SceneEventData) => { - this.addSceneEvent(`Scene Did Enter Background: ${this.getSceneDescription(args.scene)}`); - }); + private unregisterNativeWindowListeners(nativeWindow: NativeWindow) { + const events = [NativeWindowEvents.sceneWillConnect, NativeWindowEvents.sceneDidActivate, NativeWindowEvents.sceneWillResignActive, NativeWindowEvents.sceneWillEnterForeground, NativeWindowEvents.sceneDidEnterBackground, NativeWindowEvents.sceneDidDisconnect]; - Application.on(SceneEvents.sceneDidDisconnect, (args: SceneEventData) => { - this.addSceneEvent(`Scene Did Disconnect: ${this.getSceneDescription(args.scene)}`); - this.updateSceneInfo(); - }); + for (const eventName of events) { + const handlerKey = `${nativeWindow.id}:${eventName}`; + const handler = this._sceneEventHandlers.get(handlerKey); + if (handler) { + nativeWindow.off(eventName, handler); + this._sceneEventHandlers.delete(handlerKey); + } + } + } - // Listen for scene content setup events to provide content for new scenes - Application.on(SceneEvents.sceneContentSetup, (args: SceneEventData) => { - this.addSceneEvent(`Setting up content for new scene: ${this.getSceneDescription(args.scene)}`); - this.setupSceneContent(args); - }); + destroy() { + if (!__APPLE__) return; + + // Unregister window open/close listeners + if (this._windowOpenHandler) { + Application.ios.off(WindowEvents.windowOpen, this._windowOpenHandler); + } + if (this._windowCloseHandler) { + Application.ios.off(WindowEvents.windowClose, this._windowCloseHandler); + } + + // Unregister all NativeWindow listeners + for (const nativeWindow of Application.ios.getWindows()) { + this.unregisterNativeWindowListeners(nativeWindow); + } + this._sceneEventHandlers.clear(); } private getSceneDescription(scene: UIWindowScene): string { @@ -149,9 +199,12 @@ export class MultipleScenesModel extends Observable { return scene?.hash ? `${scene?.hash}` : scene?.description || 'Unknown'; } - private setupSceneContent(args: SceneEventData) { + private setupSceneContent(nativeWindow: NativeWindow, args: SceneEventData) { if (!args.scene || !args.window || !__APPLE__) return; + // Skip the primary scene (it already has content) + if (nativeWindow === Application.ios.primaryWindow) return; + try { let nsViewId: string; if (args.connectionOptions?.userActivities?.count > 0) { @@ -170,9 +223,11 @@ export class MultipleScenesModel extends Observable { // Note: can implement any number of other scene views } - console.log('setWindowRootView for:', args.window); - Application.ios.setWindowRootView(args.window, page); - this.addSceneEvent(`Content successfully set for scene: ${this.getSceneDescription(args.scene)}`); + if (page) { + console.log('setContent for window:', nativeWindow.id); + nativeWindow.setContent(page); + this.addSceneEvent(`Content successfully set for window: ${nativeWindow.id}`); + } } catch (error) { this.addSceneEvent(`Error setting up scene content: ${error.message}`); } @@ -232,8 +287,6 @@ export class MultipleScenesModel extends Observable { this._closeButtons.set(sceneId, closeButton); layout.addChild(closeButton); - // Set up the layout as a root view (this creates the native iOS view) - page._setupAsRootView({}); return page; } @@ -283,8 +336,6 @@ export class MultipleScenesModel extends Observable { this._closeButtons.set(sceneId, closeButton); layout.addChild(closeButton); - // Set up the layout as a root view (this creates the native iOS view) - page._setupAsRootView({}); return page; } @@ -334,25 +385,20 @@ export class MultipleScenesModel extends Observable { private updateSceneInfo() { if (__APPLE__ && this._isMultiSceneSupported) { try { - this._currentScenes = Application.ios.getAllScenes() || []; - - this._currentWindows = Application.ios.getAllWindows() || []; - - this._sceneCount = this._currentScenes.length; + const windows = Application.ios.getWindows() || []; + this._currentWindows = windows; + this._sceneCount = windows.length; } catch (error) { console.log('Error getting scene info:', error); this._sceneCount = 0; - this._currentScenes = []; this._currentWindows = []; } } else { this._sceneCount = 1; // Traditional single window - this._currentScenes = []; this._currentWindows = []; } this.notifyPropertyChange('sceneCount', this._sceneCount); - this.notifyPropertyChange('currentScenes', this._currentScenes); this.notifyPropertyChange('currentWindows', this._currentWindows); this.notifyPropertyChange('statusText', this.statusText); } @@ -418,7 +464,7 @@ export class MultipleScenesModel extends Observable { this.addSceneEvent(`API Test: ${apiInfo}`); // Also log current scene/window counts - this.addSceneEvent(`Current state: ${this._currentScenes.length} scenes, ${this._currentWindows.length} windows`); + this.addSceneEvent(`Current state: ${this._currentWindows.length} windows`); // Add device and system info try { diff --git a/packages/core/application/application-common.ts b/packages/core/application/application-common.ts index 50b27b6f8d..e5489f720a 100644 --- a/packages/core/application/application-common.ts +++ b/packages/core/application/application-common.ts @@ -16,6 +16,7 @@ import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessib import { getAppMainEntry, isAppInBackground, setAppInBackground, setAppMainEntry } from './helpers-common'; import { getNativeScriptGlobals } from '../globals/global-utils'; import { SDK_VERSION } from '../utils/constants'; +import type { WindowCloseEventData, WindowOpenEventData } from '../native-window'; // prettier-ignore const ORIENTATION_CSS_CLASSES = [ @@ -87,13 +88,23 @@ function applySdkVersionClass(rootView: View): void { const globalEvents = getNativeScriptGlobals().events; // Scene lifecycle event names +/** + * @deprecated Use `NativeWindowEvents` from `@nativescript/core/native-window` instead. + */ export const SceneEvents = { + /** @deprecated Use `NativeWindowEvents.sceneWillConnect` instead. */ sceneWillConnect: 'sceneWillConnect', + /** @deprecated Use `NativeWindowEvents.sceneDidActivate` instead. */ sceneDidActivate: 'sceneDidActivate', + /** @deprecated Use `NativeWindowEvents.sceneWillResignActive` instead. */ sceneWillResignActive: 'sceneWillResignActive', + /** @deprecated Use `NativeWindowEvents.sceneWillEnterForeground` instead. */ sceneWillEnterForeground: 'sceneWillEnterForeground', + /** @deprecated Use `NativeWindowEvents.sceneDidEnterBackground` instead. */ sceneDidEnterBackground: 'sceneDidEnterBackground', + /** @deprecated Use `NativeWindowEvents.sceneDidDisconnect` instead. */ sceneDidDisconnect: 'sceneDidDisconnect', + /** @deprecated Use `NativeWindowEvents.sceneContentSetup` instead. */ sceneContentSetup: 'sceneContentSetup', }; @@ -177,6 +188,9 @@ interface ApplicationEvents { on(event: 'layoutDirectionChanged', callback: (args: LayoutDirectionChangedEventData) => void, thisArg?: any): void; on(event: 'fontScaleChanged', callback: (args: FontScaleChangedEventData) => void, thisArg?: any): void; + + on(event: 'windowOpen', callback: (args: WindowOpenEventData) => void, thisArg?: any): void; + on(event: 'windowClose', callback: (args: WindowCloseEventData) => void, thisArg?: any): void; } export class ApplicationCommon { diff --git a/packages/core/application/application.android.ts b/packages/core/application/application.android.ts index 3b58ed0755..61a0cc7dea 100644 --- a/packages/core/application/application.android.ts +++ b/packages/core/application/application.android.ts @@ -8,7 +8,8 @@ import { ApplicationCommon, initializeSdkVersionClass } from './application-comm import type { AndroidActivityBundleEventData, AndroidActivityEventData, ApplicationEventData } from './application-interfaces'; import { Observable } from '../data/observable'; import { Trace } from '../trace'; -import { NativeWindow } from '../native-window/native-window.android'; +import { AndroidNativeWindow } from '../native-window/native-window.android'; +import { NativeWindow } from '../native-window/native-window-common'; import { NativeWindowEvents, WindowEvents } from '../native-window/native-window-interfaces'; import { CommonA11YServiceEnabledObservable, @@ -82,11 +83,11 @@ function initNativeScriptLifecycleCallbacks() { // Create and register NativeWindow for this activity const isPrimary = Application.android._getWindows().length === 0; - const nativeWindowId = NativeWindow.getActivityId(activity); - const nativeWindow = new NativeWindow(activity, nativeWindowId, isPrimary); + const nativeWindowId = AndroidNativeWindow.getActivityId(activity); + const nativeWindow = new AndroidNativeWindow(activity, nativeWindowId, isPrimary); Application.android._registerWindow(nativeWindow); - this.notifyActivityCreated(activity, savedInstanceState); + this.notifyActivityCreated(activity, savedInstanceState, nativeWindow); if (Application.hasListeners(Application.displayedEvent)) { this.subscribeForGlobalLayout(activity); @@ -117,9 +118,16 @@ function initNativeScriptLifecycleCallbacks() { const nativeWindow = Application.android._getWindowForActivity(activity); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.close); + // Emit activityDestroyed on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.activityDestroyed, + object: nativeWindow, + activity, + } as AndroidActivityEventData); Application.android._unregisterWindow(nativeWindow); } + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.activityDestroyedEvent, object: Application.android, @@ -144,8 +152,15 @@ function initNativeScriptLifecycleCallbacks() { const nativeWindow = Application.android._getWindowForActivity(activity); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.deactivate); + // Emit activityPaused on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.activityPaused, + object: nativeWindow, + activity, + } as AndroidActivityEventData); } + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.activityPausedEvent, object: Application.android, @@ -161,11 +176,18 @@ function initNativeScriptLifecycleCallbacks() { const nativeWindow = Application.android._getWindowForActivity(activity); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.activate); + // Emit activityResumed on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.activityResumed, + object: nativeWindow, + activity, + } as AndroidActivityEventData); } // NOTE: setSuspended(false) is called in frame/index.android.ts inside onPostResume // This is done to ensure proper timing for the event to be raised + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.activityResumedEvent, object: Application.android, @@ -177,6 +199,18 @@ function initNativeScriptLifecycleCallbacks() { public onActivitySaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, bundle: android.os.Bundle): void { // console.log('NativeScriptLifecycleCallbacks onActivitySaveInstanceState'); + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + eventName: NativeWindowEvents.saveActivityState, + object: nativeWindow, + activity, + bundle, + } as AndroidActivityBundleEventData); + } + + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.saveActivityStateEvent, object: Application.android, @@ -201,8 +235,15 @@ function initNativeScriptLifecycleCallbacks() { const nativeWindow = Application.android._getWindowForActivity(activity); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.foreground); + // Emit activityStarted on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.activityStarted, + object: nativeWindow, + activity, + } as AndroidActivityEventData); } + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.activityStartedEvent, object: Application.android, @@ -225,8 +266,15 @@ function initNativeScriptLifecycleCallbacks() { const nativeWindow = Application.android._getWindowForActivity(activity); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.background); + // Emit activityStopped on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.activityStopped, + object: nativeWindow, + activity, + } as AndroidActivityEventData); } + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.activityStoppedEvent, object: Application.android, @@ -247,7 +295,17 @@ function initNativeScriptLifecycleCallbacks() { } @profile - notifyActivityCreated(activity: androidx.appcompat.app.AppCompatActivity, bundle: android.os.Bundle) { + notifyActivityCreated(activity: androidx.appcompat.app.AppCompatActivity, bundle: android.os.Bundle, nativeWindow?: NativeWindow) { + // Emit on NativeWindow first + if (nativeWindow) { + nativeWindow.notify({ + eventName: NativeWindowEvents.activityCreated, + object: nativeWindow, + activity, + bundle, + } as AndroidActivityBundleEventData); + } + // @deprecated - Bridge to Application.android for backward compat Application.android.notify({ eventName: Application.android.activityCreatedEvent, object: Application.android, @@ -554,14 +612,14 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp } // --- NativeWindow registry --- - private _windows: NativeWindow[] = []; + private _windows: AndroidNativeWindow[] = []; /** * @internal - Register a NativeWindow created by the lifecycle callbacks. */ - _registerWindow(nativeWindow: NativeWindow): void { + _registerWindow(nativeWindow: AndroidNativeWindow): void { this._windows.push(nativeWindow); - this.notify({ + this.notify({ eventName: WindowEvents.windowOpen, object: this, window: nativeWindow, @@ -571,12 +629,12 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp /** * @internal - Unregister a NativeWindow when its activity is destroyed. */ - _unregisterWindow(nativeWindow: NativeWindow): void { + _unregisterWindow(nativeWindow: AndroidNativeWindow): void { const idx = this._windows.indexOf(nativeWindow); if (idx >= 0) { this._windows.splice(idx, 1); } - this.notify({ + this.notify({ eventName: WindowEvents.windowClose, object: this, window: nativeWindow, @@ -599,7 +657,7 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp /** * @internal - Get a NativeWindow by its activity. */ - _getWindowForActivity(activity: androidx.appcompat.app.AppCompatActivity): NativeWindow | undefined { + _getWindowForActivity(activity: androidx.appcompat.app.AppCompatActivity): AndroidNativeWindow | undefined { return this._windows.find((nw) => nw.activity === activity); } diff --git a/packages/core/application/application.d.ts b/packages/core/application/application.d.ts index f3be8c5639..6c59b93e33 100644 --- a/packages/core/application/application.d.ts +++ b/packages/core/application/application.d.ts @@ -1,6 +1,7 @@ import { ApplicationCommon } from './application-common'; import { FontScaleCategory } from '../accessibility/font-scale-common'; -import type { NativeWindowCommon } from '../native-window/native-window-common'; +import type { NativeWindow } from '../native-window/native-window-common'; +import type { WindowOpenEventData, WindowCloseEventData } from '../native-window/native-window-interfaces'; export * from './application-common'; export * from './application-interfaces'; @@ -9,60 +10,93 @@ export const Application: ApplicationCommon; export class AndroidApplication extends ApplicationCommon { /** - * @deprecated Use `Application.android.activityCreatedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityCreatedEvent = 'activityCreated'; /** - * @deprecated Use `Application.android.activityDestroyedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityDestroyedEvent = 'activityDestroyed'; /** - * @deprecated Use `Application.android.activityStartedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityStartedEvent = 'activityStarted'; /** - * @deprecated Use `Application.android.activityPausedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityPausedEvent = 'activityPaused'; /** - * @deprecated Use `Application.android.activityResumedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityResumedEvent = 'activityResumed'; /** - * @deprecated Use `Application.android.activityStoppedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityStoppedEvent = 'activityStopped'; /** - * @deprecated Use `Application.android.saveActivityStateEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly saveActivityStateEvent = 'saveActivityState'; /** - * @deprecated Use `Application.android.activityResultEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityResultEvent = 'activityResult'; /** - * @deprecated Use `Application.android.activityBackPressedEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityBackPressedEvent = 'activityBackPressed'; /** - * @deprecated Use `Application.android.activityNewIntentEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityNewIntentEvent = 'activityNewIntent'; /** - * @deprecated Use `Application.android.activityRequestPermissionsEvent` instead. + * @deprecated Listen on a NativeWindow instance instead. */ static readonly activityRequestPermissionsEvent = 'activityRequestPermissions'; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityCreatedEvent = AndroidApplication.activityCreatedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityDestroyedEvent = AndroidApplication.activityDestroyedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityStartedEvent = AndroidApplication.activityStartedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityPausedEvent = AndroidApplication.activityPausedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityResumedEvent = AndroidApplication.activityResumedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityStoppedEvent = AndroidApplication.activityStoppedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly saveActivityStateEvent = AndroidApplication.saveActivityStateEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityResultEvent = AndroidApplication.activityResultEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityBackPressedEvent = AndroidApplication.activityBackPressedEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityNewIntentEvent = AndroidApplication.activityNewIntentEvent; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ readonly activityRequestPermissionsEvent = AndroidApplication.activityRequestPermissionsEvent; getNativeApplication(): android.app.Application; @@ -137,27 +171,68 @@ export class AndroidApplication extends ApplicationCommon { */ getRegisteredBroadcastReceivers(intentFilter: string): android.content.BroadcastReceiver[]; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityStarted', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityPaused', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityResumed', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityStopped', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'saveActivityState', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityResult', callback: (args: AndroidActivityResultEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any): void; + on(event: 'windowOpen', callback: (args: WindowOpenEventData) => void, thisArg?: any): void; + on(event: 'windowClose', callback: (args: WindowCloseEventData) => void, thisArg?: any): void; + + /** + * @internal - Get a NativeWindow by its activity. + */ + _getWindowForActivity(activity: androidx.appcompat.app.AppCompatActivity): NativeWindow | undefined; + /** * Get the primary NativeWindow. */ - get primaryWindow(): NativeWindowCommon | undefined; + get primaryWindow(): NativeWindow | undefined; /** * Get all active NativeWindows. */ - getWindows(): NativeWindowCommon[]; + getWindows(): NativeWindow[]; } export class iOSApplication extends ApplicationCommon { @@ -306,15 +381,43 @@ export class iOSApplication extends ApplicationCommon { */ onSceneConfiguration: ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ + on(event: 'sceneWillConnect', callback: (args: SceneEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ + on(event: 'sceneDidActivate', callback: (args: SceneEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ + on(event: 'sceneWillResignActive', callback: (args: SceneEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ + on(event: 'sceneWillEnterForeground', callback: (args: SceneEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ + on(event: 'sceneDidEnterBackground', callback: (args: SceneEventData) => void, thisArg?: any): void; + /** + * @deprecated Listen on a NativeWindow instance instead. + */ + on(event: 'sceneDidDisconnect', callback: (args: SceneEventData) => void, thisArg?: any): void; + + on(event: 'windowOpen', callback: (args: WindowOpenEventData) => void, thisArg?: any): void; + on(event: 'windowClose', callback: (args: WindowCloseEventData) => void, thisArg?: any): void; + /** * Get the primary NativeWindow. */ - get primaryWindow(): NativeWindowCommon | undefined; + get primaryWindow(): NativeWindow | undefined; /** * Get all active NativeWindows. */ - getWindows(): NativeWindowCommon[]; + getWindows(): NativeWindow[]; /** * Flag to be set when the launch event should be delayed until the application has become active. diff --git a/packages/core/application/application.ios.ts b/packages/core/application/application.ios.ts index 0c6ad4e849..b4ab15be53 100644 --- a/packages/core/application/application.ios.ts +++ b/packages/core/application/application.ios.ts @@ -6,12 +6,13 @@ import type { NavigationEntry } from '../ui/frame/frame-interfaces'; import { getWindow } from '../utils/native-helper'; import { SDK_VERSION } from '../utils/constants'; import { ios as iosUtils, dataSerialize } from '../utils/native-helper'; -import { ApplicationCommon, initializeSdkVersionClass, SceneEvents } from './application-common'; +import { ApplicationCommon, initializeSdkVersionClass } from './application-common'; import { ApplicationEventData, SceneEventData } from './application-interfaces'; import { Observable } from '../data/observable'; import type { iOSApplication as IiOSApplication } from './application'; import { Trace } from '../trace'; -import { NativeWindow } from '../native-window/native-window.ios'; +import { IOSNativeWindow } from '../native-window/native-window.ios'; +import { NativeWindow } from '../native-window/native-window-common'; import { NativeWindowEvents, WindowEvents } from '../native-window/native-window-interfaces'; import { AccessibilityServiceEnabledPropName, @@ -225,10 +226,10 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { } const isPrimary = isFirstScene || !Application.ios.primaryWindow; - const nativeWindowId = NativeWindow.getSceneId(windowScene); + const nativeWindowId = IOSNativeWindow.getSceneId(windowScene); // Create NativeWindow and register it - const nativeWindow = new NativeWindow(windowScene, this._window, nativeWindowId, isPrimary); + const nativeWindow = new IOSNativeWindow(windowScene, this._window, nativeWindowId, isPrimary); Application.ios._registerWindow(nativeWindow); if (isPrimary) { @@ -236,9 +237,18 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { setiOSWindow(this._window); } - // Notify that scene will connect + // Notify on NativeWindow first + nativeWindow.notify({ + eventName: NativeWindowEvents.sceneWillConnect, + object: nativeWindow, + scene: windowScene, + window: this._window, + connectionOptions: connectionOptions, + } as SceneEventData); + + // @deprecated - Bridge to Application.ios for backward compat Application.ios.notify({ - eventName: SceneEvents.sceneWillConnect, + eventName: NativeWindowEvents.sceneWillConnect, object: Application.ios, scene: windowScene, window: this._window, @@ -263,8 +273,21 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.activate); + // Emit sceneDidActivate on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.sceneDidActivate, + object: nativeWindow, + scene: scene, + } as SceneEventData); } + // @deprecated - Bridge to Application.ios for backward compat + Application.ios.notify({ + eventName: NativeWindowEvents.sceneDidActivate, + object: Application.ios, + scene: scene, + } as SceneEventData); + // If this is the primary scene, trigger traditional app lifecycle if (nativeWindow?.isPrimary) { const additionalData = { @@ -285,11 +308,17 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.deactivate); + // Emit sceneWillResignActive on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.sceneWillResignActive, + object: nativeWindow, + scene: scene, + } as SceneEventData); } - // Notify that scene will resign active + // @deprecated - Bridge to Application.ios for backward compat Application.ios.notify({ - eventName: SceneEvents.sceneWillResignActive, + eventName: NativeWindowEvents.sceneWillResignActive, object: Application.ios, scene: scene, } as SceneEventData); @@ -299,10 +328,17 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.foreground); + // Emit sceneWillEnterForeground on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.sceneWillEnterForeground, + object: nativeWindow, + scene: scene, + } as SceneEventData); } + // @deprecated - Bridge to Application.ios for backward compat Application.ios.notify({ - eventName: SceneEvents.sceneWillEnterForeground, + eventName: NativeWindowEvents.sceneWillEnterForeground, object: Application.ios, scene: scene, } as SceneEventData); @@ -312,10 +348,17 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.background); + // Emit sceneDidEnterBackground on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.sceneDidEnterBackground, + object: nativeWindow, + scene: scene, + } as SceneEventData); } + // @deprecated - Bridge to Application.ios for backward compat Application.ios.notify({ - eventName: SceneEvents.sceneDidEnterBackground, + eventName: NativeWindowEvents.sceneDidEnterBackground, object: Application.ios, scene: scene, } as SceneEventData); @@ -340,11 +383,18 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate { const nativeWindow = Application.ios._getWindowForScene(scene as UIWindowScene); if (nativeWindow) { nativeWindow._notifyEvent(NativeWindowEvents.close); + // Emit sceneDidDisconnect on NativeWindow + nativeWindow.notify({ + eventName: NativeWindowEvents.sceneDidDisconnect, + object: nativeWindow, + scene: scene, + } as SceneEventData); Application.ios._unregisterWindow(nativeWindow); } + // @deprecated - Bridge to Application.ios for backward compat Application.ios.notify({ - eventName: SceneEvents.sceneDidDisconnect, + eventName: NativeWindowEvents.sceneDidDisconnect, object: Application.ios, scene: scene, } as SceneEventData); @@ -369,7 +419,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication _onSceneConfiguration: ((application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions) => UISceneConfiguration | null | undefined) | null; // NativeWindow registry - private _windows: NativeWindow[] = []; + private _windows: IOSNativeWindow[] = []; private _notificationObservers: NotificationObserver[] = []; @@ -872,9 +922,9 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication /** * @internal - Register a NativeWindow created by the SceneDelegate. */ - _registerWindow(nativeWindow: NativeWindow): void { + _registerWindow(nativeWindow: IOSNativeWindow): void { this._windows.push(nativeWindow); - this.notify({ + this.notify({ eventName: WindowEvents.windowOpen, object: this, window: nativeWindow, @@ -884,12 +934,12 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication /** * @internal - Unregister a NativeWindow when its scene disconnects. */ - _unregisterWindow(nativeWindow: NativeWindow): void { + _unregisterWindow(nativeWindow: IOSNativeWindow): void { const idx = this._windows.indexOf(nativeWindow); if (idx >= 0) { this._windows.splice(idx, 1); } - this.notify({ + this.notify({ eventName: WindowEvents.windowClose, object: this, window: nativeWindow, @@ -916,7 +966,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication /** * @internal - Get a NativeWindow by its scene. */ - _getWindowForScene(scene: UIWindowScene): NativeWindow | undefined { + _getWindowForScene(scene: UIWindowScene): IOSNativeWindow | undefined { return this._windows.find((nw) => nw.iosWindow?.scene === scene); } @@ -1182,7 +1232,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication // Try matching among known scenes for (const nw of this._windows) { const scene = nw.iosWindow?.scene; - if (scene && NativeWindow.getSceneId(scene) === target) { + if (scene && IOSNativeWindow.getSceneId(scene) === target) { return scene; } } diff --git a/packages/core/native-window/index.android.ts b/packages/core/native-window/index.android.ts deleted file mode 100644 index 83ccbf211d..0000000000 --- a/packages/core/native-window/index.android.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './native-window-interfaces'; -export * from './native-window-common'; -export * from './native-window'; diff --git a/packages/core/native-window/index.ios.ts b/packages/core/native-window/index.ios.ts deleted file mode 100644 index 83ccbf211d..0000000000 --- a/packages/core/native-window/index.ios.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './native-window-interfaces'; -export * from './native-window-common'; -export * from './native-window'; diff --git a/packages/core/native-window/index.d.ts b/packages/core/native-window/index.ts similarity index 100% rename from packages/core/native-window/index.d.ts rename to packages/core/native-window/index.ts diff --git a/packages/core/native-window/native-window-common.ts b/packages/core/native-window/native-window-common.ts index efb9f4924e..f915366c36 100644 --- a/packages/core/native-window/native-window-common.ts +++ b/packages/core/native-window/native-window-common.ts @@ -11,8 +11,9 @@ import type { StyleScope } from '../ui/styling/style-scope'; import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common'; import { SDK_VERSION } from '../utils/constants'; import { initializeSdkVersionClass } from '../application/application-common'; -import type { INativeWindow, NativeWindowEventData, NativeWindowEventName } from './native-window-interfaces'; +import type { NativeWindowEventData, NativeWindowEventName } from './native-window-interfaces'; import { NativeWindowEvents } from './native-window-interfaces'; +import type { AndroidActivityEventData, AndroidActivityBundleEventData, AndroidActivityResultEventData, AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, SceneEventData } from '../application/application-interfaces'; // prettier-ignore const ORIENTATION_CSS_CLASSES = [ @@ -43,7 +44,7 @@ let _windowIdCounter = 0; * * Platform-specific subclasses implement the abstract methods. */ -export abstract class NativeWindowCommon extends Observable implements INativeWindow { +export abstract class NativeWindow extends Observable { private _id: string; private _isPrimary: boolean; protected _rootView: View; @@ -140,14 +141,45 @@ export abstract class NativeWindowCommon extends Observable implements INativeWi return (this._layoutDirection ??= this._getLayoutDirection()); } - get iosWindow(): INativeWindow['iosWindow'] { + get iosWindow(): { readonly scene: UIWindowScene; readonly window: UIWindow } | undefined { return undefined; } - get androidWindow(): INativeWindow['androidWindow'] { + get androidWindow(): { readonly activity: androidx.appcompat.app.AppCompatActivity } | undefined { return undefined; } + // --- Typed event overloads --- + + on(event: 'activate', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'deactivate', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'background', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'foreground', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'close', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'displayed', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'contentLoaded', callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any): void; + on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + on(event: 'activityStarted', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + on(event: 'activityPaused', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + on(event: 'activityResumed', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + on(event: 'activityStopped', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void; + on(event: 'saveActivityState', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any): void; + on(event: 'activityResult', callback: (args: AndroidActivityResultEventData) => void, thisArg?: any): void; + on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any): void; + on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any): void; + on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any): void; + on(event: 'sceneWillConnect', callback: (args: SceneEventData) => void, thisArg?: any): void; + on(event: 'sceneDidActivate', callback: (args: SceneEventData) => void, thisArg?: any): void; + on(event: 'sceneWillResignActive', callback: (args: SceneEventData) => void, thisArg?: any): void; + on(event: 'sceneWillEnterForeground', callback: (args: SceneEventData) => void, thisArg?: any): void; + on(event: 'sceneDidEnterBackground', callback: (args: SceneEventData) => void, thisArg?: any): void; + on(event: 'sceneDidDisconnect', callback: (args: SceneEventData) => void, thisArg?: any): void; + on(eventName: string, callback: (data: NativeWindowEventData) => void, thisArg?: any): void; + on(eventName: string, callback: (data: any) => void, thisArg?: any): void { + super.on(eventName, callback, thisArg); + } + // Platform-specific abstract getters protected abstract _getOrientation(): 'portrait' | 'landscape' | 'unknown'; protected abstract _getSystemAppearance(): 'light' | 'dark' | null; @@ -289,7 +321,7 @@ export abstract class NativeWindowCommon extends Observable implements INativeWi _notifyEvent(eventName: NativeWindowEventName): void { this.notify({ eventName, - window: this, + window: this as any, object: this, }); } diff --git a/packages/core/native-window/native-window-interfaces.ts b/packages/core/native-window/native-window-interfaces.ts index cd8ce89d74..fce4a35a22 100644 --- a/packages/core/native-window/native-window-interfaces.ts +++ b/packages/core/native-window/native-window-interfaces.ts @@ -1,7 +1,5 @@ import type { EventData } from '../data/observable'; -import type { View } from '../ui/core/view'; -import type { NavigationEntry } from '../ui/frame/frame-interfaces'; -import type { CoreTypes } from '../core-types'; +import type { NativeWindow } from './native-window-common'; /** * Events emitted by a NativeWindow instance. @@ -21,6 +19,44 @@ export const NativeWindowEvents = { displayed: 'displayed', /** Fired when the root view content is set or changed. */ contentLoaded: 'contentLoaded', + + // iOS scene lifecycle events + /** Fired when the scene is about to connect (iOS only). */ + sceneWillConnect: 'sceneWillConnect', + /** Fired when the scene becomes active (iOS only). */ + sceneDidActivate: 'sceneDidActivate', + /** Fired when the scene is about to resign active state (iOS only). */ + sceneWillResignActive: 'sceneWillResignActive', + /** Fired when the scene is about to enter the foreground (iOS only). */ + sceneWillEnterForeground: 'sceneWillEnterForeground', + /** Fired when the scene has entered the background (iOS only). */ + sceneDidEnterBackground: 'sceneDidEnterBackground', + /** Fired when the scene has disconnected (iOS only). */ + sceneDidDisconnect: 'sceneDidDisconnect', + + // Android activity lifecycle events + /** Fired when the activity is created (Android only). */ + activityCreated: 'activityCreated', + /** Fired when the activity is destroyed (Android only). */ + activityDestroyed: 'activityDestroyed', + /** Fired when the activity is started (Android only). */ + activityStarted: 'activityStarted', + /** Fired when the activity is paused (Android only). */ + activityPaused: 'activityPaused', + /** Fired when the activity is resumed (Android only). */ + activityResumed: 'activityResumed', + /** Fired when the activity is stopped (Android only). */ + activityStopped: 'activityStopped', + /** Fired when the activity state is being saved (Android only). */ + saveActivityState: 'saveActivityState', + /** Fired when the activity receives a result (Android only). */ + activityResult: 'activityResult', + /** Fired when the back button is pressed (Android only). */ + activityBackPressed: 'activityBackPressed', + /** Fired when the activity receives a new intent (Android only). */ + activityNewIntent: 'activityNewIntent', + /** Fired when permission results are received (Android only). */ + activityRequestPermissions: 'activityRequestPermissions', } as const; export type NativeWindowEventName = (typeof NativeWindowEvents)[keyof typeof NativeWindowEvents]; @@ -40,7 +76,7 @@ export const WindowEvents = { */ export interface NativeWindowEventData extends EventData { /** The NativeWindow that emitted the event. */ - window: INativeWindow; + window: NativeWindow; } /** @@ -48,7 +84,7 @@ export interface NativeWindowEventData extends EventData { */ export interface WindowOpenEventData extends EventData { /** The NativeWindow that was opened. */ - window: INativeWindow; + window: NativeWindow; } /** @@ -56,83 +92,7 @@ export interface WindowOpenEventData extends EventData { */ export interface WindowCloseEventData extends EventData { /** The NativeWindow that was closed. */ - window: INativeWindow; -} - -/** - * Cross-platform NativeWindow interface. - * - * A NativeWindow represents a platform window surface: - * - iOS: UIWindowScene + UIWindow - * - Android: Activity - * - * Each NativeWindow manages its own root view, lifecycle events, - * and platform-specific accessors. - */ -export interface INativeWindow { - /** - * A stable identifier for this window. - * On iOS: derived from the UISceneSession persistentIdentifier. - * On Android: derived from the Activity hashCode. - */ - readonly id: string; - - /** - * Whether this is the primary (main) window. - * The first window created is typically the primary window. - * Application-level lifecycle events are bridged from the primary window. - */ - readonly isPrimary: boolean; - - /** - * The current root view of this window. - */ - readonly rootView: View; - - /** - * Set the content of this window. - * @param content A View instance, a NavigationEntry, or a module name string. - */ - setContent(content: View | NavigationEntry | string): void; - - /** - * Close this window. - * The primary window cannot be closed. - * - * iOS: requests scene session destruction. - * Android: finishes the activity. - */ - close(): void; - - /** - * The current orientation of this window. - */ - orientation(): 'portrait' | 'landscape' | 'unknown'; - - /** - * The current system appearance (light/dark) for this window. - */ - systemAppearance(): 'light' | 'dark' | null; - - /** - * The current layout direction for this window. - */ - layoutDirection(): CoreTypes.LayoutDirectionType | null; - - /** - * iOS-specific accessors. Only available when running on iOS. - */ - readonly iosWindow?: { - readonly scene: UIWindowScene; - readonly window: UIWindow; - }; - - /** - * Android-specific accessors. Only available when running on Android. - */ - readonly androidWindow?: { - readonly activity: androidx.appcompat.app.AppCompatActivity; - }; + window?: NativeWindow; } /** diff --git a/packages/core/native-window/native-window.android.ts b/packages/core/native-window/native-window.android.ts index 8676a843bd..727bf9c8cd 100644 --- a/packages/core/native-window/native-window.android.ts +++ b/packages/core/native-window/native-window.android.ts @@ -2,14 +2,13 @@ import type { View } from '../ui/core/view'; import { CoreTypes } from '../core-types'; import { SDK_VERSION } from '../utils/constants'; import { AndroidActivityCallbacks, NavigationEntry } from '../ui/frame/frame-common'; -import { NativeWindowCommon } from './native-window-common'; -import type { INativeWindow } from './native-window-interfaces'; +import { NativeWindow } from './native-window-common'; /** * Android implementation of NativeWindow. * Wraps an AppCompatActivity. */ -export class NativeWindow extends NativeWindowCommon { +export class AndroidNativeWindow extends NativeWindow { private _activity: WeakRef; constructor(activity: androidx.appcompat.app.AppCompatActivity, id: string, isPrimary = false) { @@ -24,7 +23,7 @@ export class NativeWindow extends NativeWindowCommon { return this._activity?.deref(); } - get androidWindow(): INativeWindow['androidWindow'] { + get androidWindow() { const activity = this.activity; if (!activity) { return undefined; diff --git a/packages/core/native-window/native-window.d.ts b/packages/core/native-window/native-window.d.ts deleted file mode 100644 index a089657564..0000000000 --- a/packages/core/native-window/native-window.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NativeWindowCommon } from './native-window-common'; -import type { INativeWindow, NativeWindowEventData } from './native-window-interfaces'; - -export { NativeWindowCommon } from './native-window-common'; -export * from './native-window-interfaces'; - -/** - * Cross-platform NativeWindow class. - * - * On iOS: wraps a UIWindowScene + UIWindow. - * On Android: wraps an AppCompatActivity. - * - * Use `Application.primaryWindow` to get the main window, - * or `Application.getWindows()` to get all active windows. - */ -export class NativeWindow extends NativeWindowCommon implements INativeWindow { - readonly iosWindow: - | { - readonly scene: UIWindowScene; - readonly window: UIWindow; - } - | undefined; - - readonly androidWindow: - | { - readonly activity: androidx.appcompat.app.AppCompatActivity; - } - | undefined; - // Event methods (inherited from Observable) - on(eventName: string, callback: (data: NativeWindowEventData) => void, thisArg?: any): void; - once(eventName: string, callback: (data: NativeWindowEventData) => void, thisArg?: any): void; - off(eventName: string, callback?: (data: NativeWindowEventData) => void, thisArg?: any): void; -} diff --git a/packages/core/native-window/native-window.ios.ts b/packages/core/native-window/native-window.ios.ts index f97709729a..90f55e8a2d 100644 --- a/packages/core/native-window/native-window.ios.ts +++ b/packages/core/native-window/native-window.ios.ts @@ -2,15 +2,14 @@ import type { View } from '../ui/core/view'; import { IOSHelper } from '../ui/core/view/view-helper'; import { SDK_VERSION } from '../utils/constants'; import { CoreTypes } from '../core-types'; -import { NativeWindowCommon } from './native-window-common'; +import { NativeWindow } from './native-window-common'; import { NativeWindowEvents } from './native-window-interfaces'; -import type { INativeWindow } from './native-window-interfaces'; /** * iOS implementation of NativeWindow. * Wraps a UIWindowScene + UIWindow pair. */ -export class NativeWindow extends NativeWindowCommon { +export class IOSNativeWindow extends NativeWindow { private _scene: UIWindowScene; private _window: UIWindow; @@ -20,7 +19,7 @@ export class NativeWindow extends NativeWindowCommon { this._window = window; } - get iosWindow(): INativeWindow['iosWindow'] { + get iosWindow() { return { scene: this._scene, window: this._window, diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index b76a6fd80b..629f78d29c 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -15,6 +15,7 @@ import { getAppMainEntry } from '../../application/helpers-common'; import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData } from '../../application/application-interfaces'; import { Application } from '../../application/application'; +import { NativeWindowEvents } from '../../native-window/native-window-interfaces'; import { isEmbedded, setEmbeddedView } from '../embedding'; import { CALLBACKS, FRAMEID, framesCache, setFragmentCallbacks } from './frame-helper-for-android'; import { SDK_VERSION } from '../../utils'; @@ -783,13 +784,23 @@ if (SDK_VERSION >= 33) { } const args = { - eventName: 'activityBackPressed', + eventName: NativeWindowEvents.activityBackPressed, object: Application, android: Application.android, activity: activity, cancel: false, }; + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + ...args, + object: nativeWindow, + } as AndroidActivityBackPressedEventData); + } + + // @deprecated - Bridge to Application.android for backward compat Application.android.notify(args); if (args.cancel) { @@ -804,7 +815,7 @@ if (SDK_VERSION >= 33) { if (view) { const viewArgs = { - eventName: 'activityBackPressed', + eventName: NativeWindowEvents.activityBackPressed, object: view, activity: activity, cancel: false, @@ -871,12 +882,24 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks } if (intent && intent.getAction()) { - Application.android.notify({ - eventName: Application.AndroidApplication.activityNewIntentEvent, + const newIntentArgs = { + eventName: NativeWindowEvents.activityNewIntent, object: Application.android, activity, intent, - }); + }; + + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + ...newIntentArgs, + object: nativeWindow, + } as AndroidActivityNewIntentEventData); + } + + // @deprecated - Bridge to Application.android for backward compat + Application.android.notify(newIntentArgs); } this.setActivityContent(activity, savedInstanceState, true); @@ -902,12 +925,24 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks superFunc.call(activity, intent); superSetIntentFunc.call(activity, intent); - Application.android.notify({ - eventName: Application.AndroidApplication.activityNewIntentEvent, + const newIntentArgs = { + eventName: NativeWindowEvents.activityNewIntent, object: Application.android, activity, intent, - }); + }; + + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + ...newIntentArgs, + object: nativeWindow, + } as AndroidActivityNewIntentEventData); + } + + // @deprecated - Bridge to Application.android for backward compat + Application.android.notify(newIntentArgs); } @profile @@ -996,12 +1031,23 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks } const args = { - eventName: 'activityBackPressed', + eventName: NativeWindowEvents.activityBackPressed, object: Application, android: Application.android, activity: activity, cancel: false, }; + + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + ...args, + object: nativeWindow, + } as AndroidActivityBackPressedEventData); + } + + // @deprecated - Bridge to Application.android for backward compat Application.android.notify(args); if (args.cancel) { return; @@ -1011,7 +1057,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks let callSuper = false; const viewArgs = { - eventName: 'activityBackPressed', + eventName: NativeWindowEvents.activityBackPressed, object: view, activity: activity, cancel: false, @@ -1034,15 +1080,27 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks Trace.write('NativeScriptActivity.onRequestPermissionsResult;', Trace.categories.NativeLifecycle); } - Application.android.notify({ - eventName: 'activityRequestPermissions', + const permArgs = { + eventName: NativeWindowEvents.activityRequestPermissions, object: Application, android: Application.android, activity: activity, requestCode: requestCode, permissions: permissions, grantResults: grantResults, - }); + }; + + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + ...permArgs, + object: nativeWindow, + } as AndroidActivityRequestPermissionsEventData); + } + + // @deprecated - Bridge to Application.android for backward compat + Application.android.notify(permArgs); } @profile @@ -1052,15 +1110,27 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks Trace.write(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, Trace.categories.NativeLifecycle); } - Application.android.notify({ - eventName: 'activityResult', + const resultArgs = { + eventName: NativeWindowEvents.activityResult, object: Application, android: Application.android, activity: activity, requestCode: requestCode, resultCode: resultCode, intent: data, - }); + }; + + // Emit on NativeWindow first + const nativeWindow = Application.android._getWindowForActivity(activity); + if (nativeWindow) { + nativeWindow.notify({ + ...resultArgs, + object: nativeWindow, + } as AndroidActivityResultEventData); + } + + // @deprecated - Bridge to Application.android for backward compat + Application.android.notify(resultArgs); } public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity): void { From 50f88804aebeb188e26337a81c65fda11ccbff95 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 13 Apr 2026 16:26:47 -0300 Subject: [PATCH 3/3] chore: remove unneeded scene config --- tools/assets/App_Resources/iOS/Info.plist | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tools/assets/App_Resources/iOS/Info.plist b/tools/assets/App_Resources/iOS/Info.plist index 78642afa97..53e75e7496 100644 --- a/tools/assets/App_Resources/iOS/Info.plist +++ b/tools/assets/App_Resources/iOS/Info.plist @@ -60,18 +60,5 @@ UIWindowSceneSessionRoleApplication UIApplicationSupportsMultipleScenes - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - SceneDelegate - - - -