mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Merge remote-tracking branch 'origin/main' into feat/v9-prerelease
This commit is contained in:
@@ -80,6 +80,19 @@ function applySdkVersionClass(rootView: View): void {
|
||||
|
||||
const globalEvents = getNativeScriptGlobals().events;
|
||||
|
||||
// Scene lifecycle event names
|
||||
export const SceneEvents = {
|
||||
sceneWillConnect: 'sceneWillConnect',
|
||||
sceneDidActivate: 'sceneDidActivate',
|
||||
sceneWillResignActive: 'sceneWillResignActive',
|
||||
sceneWillEnterForeground: 'sceneWillEnterForeground',
|
||||
sceneDidEnterBackground: 'sceneDidEnterBackground',
|
||||
sceneDidDisconnect: 'sceneDidDisconnect',
|
||||
sceneContentSetup: 'sceneContentSetup',
|
||||
};
|
||||
|
||||
export type SceneEventName = (typeof SceneEvents)[keyof typeof SceneEvents];
|
||||
|
||||
// helper interface to correctly type Application event handlers
|
||||
interface ApplicationEvents {
|
||||
off(eventNames: string, callback?: any, thisArg?: any): void;
|
||||
|
||||
@@ -216,3 +216,28 @@ export interface AndroidActivityBackPressedEventData extends AndroidActivityEven
|
||||
export interface LoadAppCSSEventData extends ApplicationEventData {
|
||||
cssFile: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS Event data containing information for scene lifecycle events (iOS 13+).
|
||||
*/
|
||||
export interface SceneEventData extends ApplicationEventData {
|
||||
/**
|
||||
* The UIWindowScene instance associated with this event.
|
||||
*/
|
||||
scene?: UIWindowScene;
|
||||
|
||||
/**
|
||||
* The UIWindow associated with this scene (if applicable).
|
||||
*/
|
||||
window?: UIWindow;
|
||||
|
||||
/**
|
||||
* Scene connection options (for sceneWillConnect event).
|
||||
*/
|
||||
connectionOptions?: UISceneConnectionOptions;
|
||||
|
||||
/**
|
||||
* Additional user info from the notification.
|
||||
*/
|
||||
userInfo?: NSDictionary<any, any>;
|
||||
}
|
||||
|
||||
66
packages/core/application/application.d.ts
vendored
66
packages/core/application/application.d.ts
vendored
@@ -192,6 +192,72 @@ export class iOSApplication extends ApplicationCommon {
|
||||
* @param onReceiveCallback A callback function that will be called each time the observer receives a notification.
|
||||
*/
|
||||
removeNotificationObserver(observer: any, notificationName: string);
|
||||
|
||||
/**
|
||||
* Checks if the application supports scenes.
|
||||
*/
|
||||
supportsScenes(): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the application supports multiple scenes.
|
||||
*/
|
||||
supportsMultipleScenes(): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the application is using the scene lifecycle.
|
||||
*/
|
||||
isUsingSceneLifecycle(): boolean;
|
||||
|
||||
/**
|
||||
* Opens a new window with the specified data.
|
||||
* @param data The data to pass to the new window.
|
||||
*/
|
||||
openWindow(data: Record<any, any>): void;
|
||||
|
||||
/**
|
||||
* Closes a secondary window/scene.
|
||||
* If no target is provided, attempts to close a non-primary active scene.
|
||||
* @param target Optional target to resolve the scene to close. Can be a View, UIWindow, UIWindowScene, or a string scene identifier.
|
||||
*/
|
||||
closeWindow(target?: View | UIWindow | UIWindowScene | string): void;
|
||||
|
||||
/**
|
||||
* Gets all windows for the application.
|
||||
*/
|
||||
getAllWindows(): UIWindow[];
|
||||
|
||||
/**
|
||||
* Gets all scenes for the application.
|
||||
*/
|
||||
getAllScenes(): UIScene[];
|
||||
|
||||
/**
|
||||
* Gets all window scenes for the application.
|
||||
*/
|
||||
getWindowScenes(): UIWindowScene[];
|
||||
|
||||
/**
|
||||
* Gets the primary window for the application.
|
||||
*/
|
||||
getPrimaryWindow(): UIWindow;
|
||||
|
||||
/**
|
||||
* Gets the primary scene for the application.
|
||||
*/
|
||||
getPrimaryScene(): UIWindowScene;
|
||||
|
||||
/**
|
||||
* Sets the root view for a specific window.
|
||||
* @param window The window to set the root view for.
|
||||
* @param view The view to set as the root view.
|
||||
*/
|
||||
setWindowRootView(window: UIWindow, view: View): void;
|
||||
|
||||
/**
|
||||
* The scene delegate for the application.
|
||||
* Get the current one or set a custom one.
|
||||
*/
|
||||
sceneDelegate: UIWindowSceneDelegate;
|
||||
}
|
||||
|
||||
export const VALID_FONT_SCALES: number[];
|
||||
|
||||
@@ -5,9 +5,9 @@ import { IOSHelper } from '../ui/core/view/view-helper';
|
||||
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
|
||||
import { getWindow } from '../utils/native-helper';
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
import { ios as iosUtils } from '../utils/native-helper';
|
||||
import { ApplicationCommon, initializeSdkVersionClass } from './application-common';
|
||||
import { ApplicationEventData } from './application-interfaces';
|
||||
import { ios as iosUtils, dataSerialize } from '../utils/native-helper';
|
||||
import { ApplicationCommon, initializeSdkVersionClass, SceneEvents } from './application-common';
|
||||
import { ApplicationEventData, SceneEventData } from './application-interfaces';
|
||||
import { Observable } from '../data/observable';
|
||||
import { Trace } from '../trace';
|
||||
import {
|
||||
@@ -81,6 +81,31 @@ class CADisplayLinkTarget extends NSObject {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the app supports scenes.
|
||||
* When an app configures UIApplicationSceneManifest in Info.plist
|
||||
* it will use scene lifecycle management.
|
||||
*/
|
||||
let sceneManifest: NSDictionary<any, any>;
|
||||
function supportsScenes(): boolean {
|
||||
if (SDK_VERSION < 13) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof sceneManifest === 'undefined') {
|
||||
// Check if scene manifest exists in Info.plist
|
||||
sceneManifest = NSBundle.mainBundle.objectForInfoDictionaryKey('UIApplicationSceneManifest');
|
||||
}
|
||||
return !!sceneManifest;
|
||||
}
|
||||
|
||||
function supportsMultipleScenes(): boolean {
|
||||
if (SDK_VERSION < 13) {
|
||||
return false;
|
||||
}
|
||||
return UIApplication.sharedApplication?.supportsMultipleScenes;
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class Responder extends UIResponder implements UIApplicationDelegate {
|
||||
get window(): UIWindow {
|
||||
@@ -94,10 +119,129 @@ class Responder extends UIResponder implements UIApplicationDelegate {
|
||||
static ObjCProtocols = [UIApplicationDelegate];
|
||||
}
|
||||
|
||||
if (supportsScenes()) {
|
||||
/**
|
||||
* This method is called when a new scene session is being created.
|
||||
* Important: When this method is implemented, the app assumes scene-based lifecycle management.
|
||||
* 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.
|
||||
*/
|
||||
(Responder.prototype as UIApplicationDelegate).applicationConfigurationForConnectingSceneSessionOptions = function (application: UIApplication, connectingSceneSession: UISceneSession, options: UISceneConnectionOptions): UISceneConfiguration {
|
||||
const config = UISceneConfiguration.configurationWithNameSessionRole('Default Configuration', connectingSceneSession.role);
|
||||
config.sceneClass = UIWindowScene as any;
|
||||
config.delegateClass = SceneDelegate;
|
||||
return config;
|
||||
};
|
||||
|
||||
// scene session destruction handling
|
||||
(Responder.prototype as UIApplicationDelegate).applicationDidDiscardSceneSessions = function (application: UIApplication, sceneSessions: NSSet<UISceneSession>): void {
|
||||
// Note: we could emit an event here if needed
|
||||
// console.log('Scene sessions discarded:', sceneSessions.count);
|
||||
};
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
|
||||
static ObjCProtocols = [UIWindowSceneDelegate];
|
||||
|
||||
private _window: UIWindow;
|
||||
private _scene: UIWindowScene;
|
||||
|
||||
get window(): UIWindow {
|
||||
return this._window;
|
||||
}
|
||||
|
||||
set window(value: UIWindow) {
|
||||
this._window = value;
|
||||
}
|
||||
|
||||
sceneWillConnectToSessionOptions(scene: UIScene, session: UISceneSession, connectionOptions: UISceneConnectionOptions): void {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`SceneDelegate.sceneWillConnectToSessionOptions called with role: ${session.role}`, Trace.categories.NativeLifecycle);
|
||||
}
|
||||
|
||||
if (!(scene instanceof UIWindowScene)) {
|
||||
// Scene is not a UIWindowScene, ignoring
|
||||
return;
|
||||
}
|
||||
|
||||
this._scene = scene;
|
||||
|
||||
// Create window for this scene
|
||||
this._window = UIWindow.alloc().initWithWindowScene(scene);
|
||||
|
||||
// Store the window scene for this window
|
||||
Application.ios._setWindowForScene(this._window, scene);
|
||||
|
||||
// Set up the window content
|
||||
Application.ios._setupWindowForScene(this._window, scene);
|
||||
|
||||
// Notify that scene will connect
|
||||
Application.ios.notify({
|
||||
eventName: SceneEvents.sceneWillConnect,
|
||||
object: Application.ios,
|
||||
scene: scene,
|
||||
window: this._window,
|
||||
connectionOptions: connectionOptions,
|
||||
} as SceneEventData);
|
||||
|
||||
if (scene === Application.ios.getPrimaryScene()) {
|
||||
// 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 (!Application.ios.getPrimaryScene()) {
|
||||
Application.ios._notifySceneAppStarted();
|
||||
}
|
||||
}
|
||||
sceneDidBecomeActive(scene: UIScene): void {
|
||||
// This will be handled by the notification observer in iOSApplication
|
||||
// The notification system will automatically trigger sceneDidActivate
|
||||
}
|
||||
|
||||
sceneWillResignActive(scene: UIScene): void {
|
||||
// Notify that scene will resign active
|
||||
Application.ios.notify({
|
||||
eventName: SceneEvents.sceneWillResignActive,
|
||||
object: Application.ios,
|
||||
scene: scene,
|
||||
} as SceneEventData);
|
||||
}
|
||||
|
||||
sceneWillEnterForeground(scene: UIScene): void {
|
||||
// This will be handled by the notification observer in iOSApplication
|
||||
}
|
||||
|
||||
sceneDidEnterBackground(scene: UIScene): void {
|
||||
// This will be handled by the notification observer in iOSApplication
|
||||
}
|
||||
|
||||
sceneDidDisconnect(scene: UIScene): void {
|
||||
// This will be handled by the notification observer in iOSApplication
|
||||
}
|
||||
}
|
||||
// ensure available globally
|
||||
global.SceneDelegate = SceneDelegate;
|
||||
|
||||
export class iOSApplication extends ApplicationCommon {
|
||||
private _delegate: UIApplicationDelegate;
|
||||
private _delegateHandlers = new Map<string, Array<Function>>();
|
||||
private _rootView: View;
|
||||
private _sceneDelegate: UIWindowSceneDelegate;
|
||||
private _windowSceneMap = new Map<UIScene, UIWindow>();
|
||||
private _primaryScene: UIWindowScene | null = null;
|
||||
private _openedScenesById = new Map<string, UIWindowScene>();
|
||||
|
||||
displayedOnce = false;
|
||||
displayedLinkTarget: CADisplayLinkTarget;
|
||||
@@ -115,6 +259,15 @@ export class iOSApplication extends ApplicationCommon {
|
||||
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 {
|
||||
@@ -388,6 +541,11 @@ export class iOSApplication extends ApplicationCommon {
|
||||
}
|
||||
}
|
||||
|
||||
// Public method for scene-based app startup
|
||||
_notifySceneAppStarted() {
|
||||
this.notifyAppStarted();
|
||||
}
|
||||
|
||||
public _onLivesync(context?: ModuleContext): void {
|
||||
// Handle application root module
|
||||
const isAppRootModuleChanged = context && context.path && context.path.includes(this.getMainEntry().moduleName) && context.type !== 'style';
|
||||
@@ -456,19 +614,26 @@ export class iOSApplication extends ApplicationCommon {
|
||||
}
|
||||
}
|
||||
this.setMaxRefreshRate();
|
||||
// ensures window is assigned to proper window scene
|
||||
setiOSWindow(this.window);
|
||||
|
||||
if (!getiOSWindow()) {
|
||||
// if still no window, create one
|
||||
setiOSWindow(UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds));
|
||||
// Only set up window if NOT using scene-based lifecycle
|
||||
if (!this.supportsScenes()) {
|
||||
// Traditional single-window app setup
|
||||
// ensures window is assigned to proper window scene
|
||||
setiOSWindow(this.window);
|
||||
|
||||
if (!getiOSWindow()) {
|
||||
// if still no window, create one
|
||||
setiOSWindow(UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds));
|
||||
}
|
||||
|
||||
if (!__VISIONOS__) {
|
||||
this.window.backgroundColor = SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor;
|
||||
}
|
||||
|
||||
this.notifyAppStarted(notification);
|
||||
} else {
|
||||
// Scene-based app - window creation will happen in scene delegate
|
||||
}
|
||||
|
||||
if (!__VISIONOS__) {
|
||||
this.window.backgroundColor = SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor;
|
||||
}
|
||||
|
||||
this.notifyAppStarted(notification);
|
||||
}
|
||||
|
||||
@profile
|
||||
@@ -525,6 +690,489 @@ export class iOSApplication extends ApplicationCommon {
|
||||
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;
|
||||
}
|
||||
|
||||
this.notify({
|
||||
eventName: SceneEvents.sceneWillConnect,
|
||||
object: this,
|
||||
scene: scene,
|
||||
userInfo: notification.userInfo,
|
||||
} as SceneEventData);
|
||||
}
|
||||
|
||||
private sceneDidActivate(notification: NSNotification) {
|
||||
const scene = notification.object as UIScene;
|
||||
this.notify({
|
||||
eventName: SceneEvents.sceneDidActivate,
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sceneWillEnterForeground(notification: NSNotification) {
|
||||
const scene = notification.object as UIScene;
|
||||
this.notify({
|
||||
eventName: SceneEvents.sceneWillEnterForeground,
|
||||
object: this,
|
||||
scene: scene,
|
||||
} as SceneEventData);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.notify({
|
||||
eventName: SceneEvents.sceneDidDisconnect,
|
||||
object: this,
|
||||
scene: scene,
|
||||
} as SceneEventData);
|
||||
}
|
||||
|
||||
// Scene management helper methods
|
||||
_setWindowForScene(window: UIWindow, scene: UIScene): void {
|
||||
this._windowSceneMap.set(scene, window);
|
||||
}
|
||||
|
||||
_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 {}
|
||||
}
|
||||
|
||||
_getWindowForScene(scene: UIScene): UIWindow | undefined {
|
||||
return this._windowSceneMap.get(scene);
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
// Set up the window content for the primary scene
|
||||
this.setWindowContent();
|
||||
}
|
||||
}
|
||||
|
||||
get sceneDelegate(): UIWindowSceneDelegate {
|
||||
if (!this._sceneDelegate) {
|
||||
this._sceneDelegate = SceneDelegate.new() as UIWindowSceneDelegate;
|
||||
}
|
||||
return this._sceneDelegate;
|
||||
}
|
||||
|
||||
set sceneDelegate(value: UIWindowSceneDelegate) {
|
||||
this._sceneDelegate = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-window support
|
||||
*/
|
||||
|
||||
/**
|
||||
* Opens a new window with the specified data.
|
||||
* @param data The data to pass to the new window.
|
||||
*/
|
||||
openWindow(data: Record<any, any>) {
|
||||
if (!supportsMultipleScenes()) {
|
||||
console.log('Cannot create a new scene - not supported on this device.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const app = UIApplication.sharedApplication;
|
||||
|
||||
// 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);
|
||||
|
||||
request.options = options;
|
||||
} catch (roleError) {
|
||||
console.log('Error creating request:', roleError);
|
||||
return;
|
||||
}
|
||||
|
||||
app.activateSceneSessionForRequestErrorHandler(request, (error) => {
|
||||
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) {
|
||||
this.createSceneWithLegacyAPI(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 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 {
|
||||
console.log('Neither new nor legacy scene activation methods are available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error requesting new scene:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
*/
|
||||
public closeWindow(target?: 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;
|
||||
}
|
||||
|
||||
// Don't allow closing the primary scene
|
||||
if (scene === this._primaryScene) {
|
||||
console.log('closeWindow: Refusing to close the primary scene');
|
||||
return;
|
||||
}
|
||||
|
||||
const session = scene.session;
|
||||
if (!session) {
|
||||
console.log('closeWindow: Scene has no session to destroy');
|
||||
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');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('closeWindow: Unexpected error', err);
|
||||
}
|
||||
}
|
||||
|
||||
getAllWindows(): UIWindow[] {
|
||||
return Array.from(this._windowSceneMap.values());
|
||||
}
|
||||
|
||||
getAllScenes(): UIScene[] {
|
||||
return Array.from(this._windowSceneMap.keys());
|
||||
}
|
||||
|
||||
getWindowScenes(): UIWindowScene[] {
|
||||
return this.getAllScenes().filter((scene) => scene instanceof UIWindowScene) as UIWindowScene[];
|
||||
}
|
||||
|
||||
getPrimaryWindow(): UIWindow {
|
||||
if (this._primaryScene) {
|
||||
return this._getWindowForScene(this._primaryScene) || getiOSWindow();
|
||||
}
|
||||
return getiOSWindow();
|
||||
}
|
||||
|
||||
getPrimaryScene(): UIWindowScene | null {
|
||||
return this._primaryScene;
|
||||
}
|
||||
|
||||
// Scene lifecycle management
|
||||
supportsScenes(): boolean {
|
||||
return supportsScenes();
|
||||
}
|
||||
|
||||
supportsMultipleScenes(): boolean {
|
||||
return supportsMultipleScenes();
|
||||
}
|
||||
|
||||
isUsingSceneLifecycle(): boolean {
|
||||
return this.supportsScenes() && this._windowSceneMap.size > 0;
|
||||
}
|
||||
|
||||
// Call this to set up scene-based configuration
|
||||
configureForScenes(): void {
|
||||
if (!this.supportsScenes()) {
|
||||
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
|
||||
private _resolveScene(target?: any): UIWindowScene | null {
|
||||
if (!__APPLE__) {
|
||||
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;
|
||||
}
|
||||
// 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) {
|
||||
return target as UIWindowScene;
|
||||
}
|
||||
// UIWindow
|
||||
if ((target as UIWindow).windowScene) {
|
||||
return (target as UIWindow).windowScene;
|
||||
}
|
||||
// NativeScript View
|
||||
if ((target as View)?.nativeViewProtected) {
|
||||
const uiView = (target as View).nativeViewProtected as UIView;
|
||||
const win = uiView?.window as UIWindow;
|
||||
return win?.windowScene || null;
|
||||
}
|
||||
}
|
||||
// String id lookup
|
||||
if (typeof target === 'string') {
|
||||
if (this._openedScenesById.has(target)) {
|
||||
return this._openedScenesById.get(target);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private createSceneWithLegacyAPI(data: Record<any, any>) {
|
||||
const windowScene = this.window?.windowScene;
|
||||
|
||||
if (!windowScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create user activity for the new scene
|
||||
const userActivity = NSUserActivity.alloc().initWithActivityType(`${NSBundle.mainBundle.bundleIdentifier}.scene`);
|
||||
userActivity.userInfo = dataSerialize(data);
|
||||
|
||||
// Use the legacy API
|
||||
const options = UISceneActivationRequestOptions.new();
|
||||
options.requestingScene = windowScene;
|
||||
|
||||
UIApplication.sharedApplication.requestSceneSessionActivationUserActivityOptionsErrorHandler(
|
||||
null, // session - null for new scene
|
||||
userActivity,
|
||||
options,
|
||||
(error: NSError) => {
|
||||
if (error) {
|
||||
console.error(`Legacy scene API failed: ${error.localizedDescription}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple view controller with a NativeScript view for a scene window.
|
||||
* @param window The UIWindow to set content for
|
||||
* @param view The NativeScript View to set as root content
|
||||
*/
|
||||
setWindowRootView(window: UIWindow, view: View): void {
|
||||
if (!window || !view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.ios) {
|
||||
window.rootViewController = view.viewController;
|
||||
window.makeKeyAndVisible();
|
||||
} else {
|
||||
console.warn('View does not have a native iOS implementation');
|
||||
}
|
||||
}
|
||||
|
||||
get ios() {
|
||||
// ensures Application.ios is defined when running on iOS
|
||||
return this;
|
||||
|
||||
@@ -23,7 +23,8 @@ function getConsumer(mapPath: string, sourceMap: any) {
|
||||
c = new SourceMapConsumer(sourceMap);
|
||||
consumerCache.set(mapPath, c);
|
||||
} catch (error) {
|
||||
console.error(`Failed to create SourceMapConsumer for ${mapPath}:`, error);
|
||||
// Keep quiet in production-like console; failures just fall back to original stack
|
||||
console.debug && console.debug(`SourceMapConsumer failed for ${mapPath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +68,7 @@ function loadAndExtractMap(mapPath: string) {
|
||||
mapText = binary;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load source map ${mapPath}:`, error);
|
||||
console.debug && console.debug(`Failed to load source map ${mapPath}:`, error);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@@ -108,7 +109,7 @@ function remapFrame(file: string, line: number, column: number) {
|
||||
try {
|
||||
return consumer.originalPositionFor({ line, column });
|
||||
} catch (error) {
|
||||
console.error(`Failed to get original position for ${file}:${line}:${column}:`, error);
|
||||
console.debug && console.debug(`Remap failed for ${file}:${line}:${column}:`, error);
|
||||
return { source: null, line: 0, column: 0 };
|
||||
}
|
||||
}
|
||||
@@ -116,18 +117,36 @@ function remapFrame(file: string, line: number, column: number) {
|
||||
function remapStack(raw: string): string {
|
||||
const lines = raw.split('\n');
|
||||
const out = lines.map((line) => {
|
||||
const m = /\((.+):(\d+):(\d+)\)/.exec(line);
|
||||
if (!m) return line;
|
||||
|
||||
try {
|
||||
const [_, file, l, c] = m;
|
||||
const orig = remapFrame(file, +l, +c);
|
||||
if (!orig.source) return line;
|
||||
return line.replace(/\(.+\)/, `(${orig.source}:${orig.line}:${orig.column})`);
|
||||
} catch (error) {
|
||||
console.error('Failed to remap stack frame:', line, error);
|
||||
return line; // return original line if remapping fails
|
||||
// 1) Parenthesized frame: at fn (file:...:L:C)
|
||||
let m = /\((.+):(\d+):(\d+)\)/.exec(line);
|
||||
if (m) {
|
||||
try {
|
||||
const [_, file, l, c] = m;
|
||||
const orig = remapFrame(file, +l, +c);
|
||||
if (!orig.source) return line;
|
||||
return line.replace(/\(.+\)/, `(${orig.source}:${orig.line}:${orig.column})`);
|
||||
} catch (error) {
|
||||
console.debug && console.debug('Remap failed for frame:', line, error);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Bare frame: at file:///app/vendor.js:L:C (no parentheses)
|
||||
const bare = /(\s+at\s+)([^\s()]+):(\d+):(\d+)/.exec(line);
|
||||
if (bare) {
|
||||
try {
|
||||
const [, prefix, file, l, c] = bare;
|
||||
const orig = remapFrame(file, +l, +c);
|
||||
if (!orig.source) return line;
|
||||
const replacement = `${prefix}${orig.source}:${orig.line}:${orig.column}`;
|
||||
return line.replace(bare[0], replacement);
|
||||
} catch (error) {
|
||||
console.debug && console.debug('Remap failed for bare frame:', line, error);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
});
|
||||
return out.join('\n');
|
||||
}
|
||||
@@ -141,7 +160,7 @@ function remapStack(raw: string): string {
|
||||
try {
|
||||
return remapStack(rawStack);
|
||||
} catch (error) {
|
||||
console.error('Failed to remap stack trace, returning original:', error);
|
||||
console.debug && console.debug('Remap failed, returning original:', error);
|
||||
return rawStack; // fallback to original stack trace
|
||||
}
|
||||
};
|
||||
|
||||
@@ -917,7 +917,6 @@ export class View extends ViewCommon {
|
||||
if (!navigationBar) return;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
console.log('here:', value);
|
||||
navigationBar.barStyle = value === 'dark' ? UIBarStyle.Black : UIBarStyle.Default;
|
||||
} else {
|
||||
navigationBar.barStyle = value;
|
||||
|
||||
@@ -186,6 +186,11 @@ export class TextBase extends TextBaseCommon {
|
||||
super.initNativeView();
|
||||
initializeTextTransformation();
|
||||
const nativeView = this.nativeTextViewProtected;
|
||||
|
||||
// Fix for custom font over-height issue on Android
|
||||
// Disable font padding to prevent extra spacing around text
|
||||
nativeView.setIncludeFontPadding(false);
|
||||
|
||||
this._defaultTransformationMethod = nativeView.getTransformationMethod();
|
||||
this._defaultMovementMethod = nativeView.getMovementMethod();
|
||||
this._minHeight = nativeView.getMinHeight();
|
||||
|
||||
@@ -16,6 +16,14 @@ global.__APPLE__ = true;
|
||||
global.__COMMONJS__ = false;
|
||||
global.WeakRef.prototype.get = global.WeakRef.prototype.deref;
|
||||
global.NativeClass = function () {};
|
||||
global.NSBundle = {
|
||||
mainBundle: {
|
||||
bundleIdentifier: 'test',
|
||||
objectForInfoDictionaryKey(key: string) {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
global.NSTimer = class NSTimer {};
|
||||
global.NSObject = class NSObject {
|
||||
static new() {
|
||||
@@ -88,6 +96,7 @@ global.UIInterfaceOrientation = {
|
||||
LandscapeLeft: 4,
|
||||
LandscapeRight: 3,
|
||||
};
|
||||
global.UIWindowSceneDelegate = function () {};
|
||||
const cgColors = { CGColor: 1 };
|
||||
global.UIColor = {
|
||||
alloc() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { applyDotEnvPlugin } from '../helpers/dotEnv';
|
||||
import { env as _env, IWebpackEnv } from '../index';
|
||||
import { getValue } from '../helpers/config';
|
||||
import { getIPS } from '../helpers/host';
|
||||
import FixSourceMapUrlPlugin from '../plugins/FixSourceMapUrlPlugin';
|
||||
import {
|
||||
getAvailablePlatforms,
|
||||
getAbsoluteDistPath,
|
||||
@@ -178,32 +179,9 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
|
||||
|
||||
// For ESM builds, fix the sourceMappingURL to use correct paths
|
||||
if (!env.commonjs && sourceMapType && sourceMapType !== 'hidden-source-map') {
|
||||
class FixSourceMapUrlPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tap('FixSourceMapUrlPlugin', (compilation) => {
|
||||
const leadingCharacter = process.platform === "win32" ? "/":"";
|
||||
Object.keys(compilation.assets).forEach((filename) => {
|
||||
if (filename.endsWith('.mjs') || filename.endsWith('.js')) {
|
||||
const asset = compilation.assets[filename];
|
||||
let source = asset.source();
|
||||
|
||||
// Replace sourceMappingURL to use file:// protocol pointing to actual location
|
||||
source = source.replace(
|
||||
/\/\/# sourceMappingURL=(.+\.map)/g,
|
||||
`//# sourceMappingURL=file://${leadingCharacter}${outputPath}/$1`,
|
||||
);
|
||||
|
||||
compilation.assets[filename] = {
|
||||
source: () => source,
|
||||
size: () => source.length,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
config.plugin('FixSourceMapUrlPlugin').use(FixSourceMapUrlPlugin);
|
||||
config
|
||||
.plugin('FixSourceMapUrlPlugin')
|
||||
.use(FixSourceMapUrlPlugin as any, [{ outputPath }]);
|
||||
}
|
||||
|
||||
// when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder
|
||||
|
||||
112
packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts
Normal file
112
packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { Compiler } from 'webpack';
|
||||
import { sources } from 'webpack';
|
||||
|
||||
export interface FixSourceMapUrlPluginOptions {
|
||||
outputPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures sourceMappingURL points to the actual file:// location on device/emulator.
|
||||
* Handles Webpack 5 asset sources (string/Buffer/Source objects).
|
||||
*/
|
||||
export default class FixSourceMapUrlPlugin {
|
||||
constructor(private readonly options: FixSourceMapUrlPluginOptions) {}
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
const wp: any = (compiler as any).webpack;
|
||||
const hasProcessAssets =
|
||||
!!wp?.Compilation?.PROCESS_ASSETS_STAGE_DEV_TOOLING &&
|
||||
!!(compiler as any).hooks?.thisCompilation;
|
||||
|
||||
const leadingCharacter = process.platform === 'win32' ? '/' : '';
|
||||
|
||||
const toStringContent = (content: any): string => {
|
||||
if (typeof content === 'string') return content;
|
||||
if (Buffer.isBuffer(content)) return content.toString('utf-8');
|
||||
if (content && typeof content.source === 'function') {
|
||||
const inner = content.source();
|
||||
if (typeof inner === 'string') return inner;
|
||||
if (Buffer.isBuffer(inner)) return inner.toString('utf-8');
|
||||
try {
|
||||
return String(inner);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
try {
|
||||
return String(content);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = (filename: string, compilation: any) => {
|
||||
if (!(filename.endsWith('.mjs') || filename.endsWith('.js'))) return;
|
||||
|
||||
// Support both legacy compilation.assets and v5 Asset API
|
||||
let rawSource: any;
|
||||
if (typeof (compilation as any).getAsset === 'function') {
|
||||
const assetObj = (compilation as any).getAsset(filename);
|
||||
if (assetObj && assetObj.source) {
|
||||
rawSource = (assetObj.source as any).source
|
||||
? (assetObj.source as any).source()
|
||||
: (assetObj.source as any)();
|
||||
}
|
||||
}
|
||||
if (
|
||||
rawSource === undefined &&
|
||||
(compilation as any).assets &&
|
||||
(compilation as any).assets[filename]
|
||||
) {
|
||||
const asset = (compilation as any).assets[filename];
|
||||
rawSource = typeof asset.source === 'function' ? asset.source() : asset;
|
||||
}
|
||||
|
||||
let source = toStringContent(rawSource);
|
||||
// Replace sourceMappingURL to use file:// protocol pointing to actual location
|
||||
source = source.replace(
|
||||
/\/\/\# sourceMappingURL=(.+\.map)/g,
|
||||
`//# sourceMappingURL=file://${leadingCharacter}${this.options.outputPath}/$1`,
|
||||
);
|
||||
|
||||
// Prefer Webpack 5 updateAsset with RawSource when available
|
||||
const RawSourceCtor =
|
||||
wp?.sources?.RawSource || (sources as any)?.RawSource;
|
||||
if (
|
||||
typeof (compilation as any).updateAsset === 'function' &&
|
||||
RawSourceCtor
|
||||
) {
|
||||
(compilation as any).updateAsset(filename, new RawSourceCtor(source));
|
||||
} else {
|
||||
(compilation as any).assets[filename] = {
|
||||
source: () => source,
|
||||
size: () => source.length,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (hasProcessAssets) {
|
||||
compiler.hooks.thisCompilation.tap(
|
||||
'FixSourceMapUrlPlugin',
|
||||
(compilation: any) => {
|
||||
const stage = wp.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING;
|
||||
compilation.hooks.processAssets.tap(
|
||||
{ name: 'FixSourceMapUrlPlugin', stage },
|
||||
(assets: Record<string, any>) => {
|
||||
Object.keys(assets).forEach((filename) =>
|
||||
processFile(filename, compilation),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Fallback for older setups: use emit (may log deprecation in newer webpack)
|
||||
compiler.hooks.emit.tap('FixSourceMapUrlPlugin', (compilation: any) => {
|
||||
Object.keys((compilation as any).assets).forEach((filename) =>
|
||||
processFile(filename, compilation),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user