Files
NativeScript/apps/toolbox/src/pages/multiple-scenes.ts
2025-11-03 17:01:43 -08:00

524 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Observable, EventData, Page, Application, Frame, StackLayout, Label, Button, Dialogs, View, Color, SceneEvents, SceneEventData, Utils } from '@nativescript/core';
let page: Page;
let viewModel: MultipleScenesModel;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
viewModel = new MultipleScenesModel();
page.bindingContext = viewModel;
}
export class MultipleScenesModel extends Observable {
private _sceneCount = 0;
private _isMultiSceneSupported = false;
private _currentScenes: any[] = [];
private _currentWindows: any[] = [];
private _sceneEvents: string[] = [];
constructor() {
super();
this.checkSceneSupport();
this.setupSceneEventListeners();
this.updateSceneInfo();
this.checkSceneDelegateRegistration();
}
get sceneCount(): number {
return this._sceneCount;
}
get isMultiSceneSupported(): boolean {
return this._isMultiSceneSupported;
}
get currentScenes(): any[] {
return this._currentScenes;
}
get currentWindows(): any[] {
return this._currentWindows;
}
get sceneEvents(): string[] {
return this._sceneEvents;
}
get canCreateNewScene(): boolean {
return this._isMultiSceneSupported && __APPLE__;
}
get statusText(): string {
if (!__APPLE__) {
return 'Scene support is only available on iOS';
}
if (!this._isMultiSceneSupported) {
return 'Multi-scene support not enabled. Add scene configuration to Info.plist';
}
// Check which API is available
let apiInfo = '';
try {
if (typeof UIApplication !== 'undefined') {
const app = UIApplication.sharedApplication;
if (typeof app.activateSceneSessionForRequestErrorHandler === 'function') {
apiInfo = ' (iOS 17+ API available)';
} else if (typeof app.requestSceneSessionActivationUserActivityOptionsErrorHandler === 'function') {
apiInfo = ' (iOS 13-16 API available)';
}
}
} catch (e) {
// Ignore errors in API detection
}
return `Multi-scene support enabled. ${this._sceneCount} scene(s) active${apiInfo}`;
}
private checkSceneSupport() {
if (__APPLE__) {
try {
// Check if the supportsScenes method exists and call it
if (typeof Application.ios.supportsScenes === 'function') {
this._isMultiSceneSupported = Application.ios.supportsScenes();
} else {
// Fallback: check for scene manifest in bundle
this._isMultiSceneSupported = false;
try {
const bundle = NSBundle.mainBundle;
const sceneManifest = bundle.objectForInfoDictionaryKey('UIApplicationSceneManifest');
this._isMultiSceneSupported = !!sceneManifest;
} catch (e) {
console.log('Error checking scene manifest:', e);
}
}
console.log('Scene support check:', this._isMultiSceneSupported);
} catch (error) {
console.log('Error checking scene support:', error);
this._isMultiSceneSupported = false;
}
}
this.notifyPropertyChange('isMultiSceneSupported', this._isMultiSceneSupported);
this.notifyPropertyChange('canCreateNewScene', this.canCreateNewScene);
this.notifyPropertyChange('statusText', this.statusText);
}
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)}`);
this.updateSceneInfo();
});
Application.on(SceneEvents.sceneDidActivate, (args: SceneEventData) => {
this.addSceneEvent(`Scene Did Activate: ${this.getSceneDescription(args.scene)}`);
this.updateSceneInfo();
});
Application.on(SceneEvents.sceneWillResignActive, (args: SceneEventData) => {
this.addSceneEvent(`Scene Will Resign Active: ${this.getSceneDescription(args.scene)}`);
});
Application.on(SceneEvents.sceneWillEnterForeground, (args: SceneEventData) => {
this.addSceneEvent(`Scene Will Enter Foreground: ${this.getSceneDescription(args.scene)}`);
});
Application.on(SceneEvents.sceneDidEnterBackground, (args: SceneEventData) => {
this.addSceneEvent(`Scene Did Enter Background: ${this.getSceneDescription(args.scene)}`);
});
Application.on(SceneEvents.sceneDidDisconnect, (args: SceneEventData) => {
this.addSceneEvent(`Scene Did Disconnect: ${this.getSceneDescription(args.scene)}`);
this.updateSceneInfo();
});
// 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);
});
}
private getSceneDescription(scene: UIWindowScene): string {
if (!scene) return 'Unknown';
return `Scene ${this.getSceneId(scene)}`;
}
private getSceneId(scene: UIWindowScene): string {
return scene?.hash ? `${scene?.hash}` : scene?.description || 'Unknown';
}
private setupSceneContent(args: SceneEventData) {
if (!args.scene || !args.window || !__APPLE__) return;
try {
let nsViewId: string;
if (args.connectionOptions?.userActivities?.count > 0) {
const activity = args.connectionOptions.userActivities.allObjects.objectAtIndex(0) as NSUserActivity;
nsViewId = Utils.dataDeserialize(activity.userInfo).id;
}
console.log('--- Open scene for nsViewId:', nsViewId);
let page: Page;
switch (nsViewId) {
case 'newSceneBasic':
page = this._createPageForScene(args.scene, args.window);
break;
case 'newSceneAlt':
page = this._createAltPageForScene(args.scene, args.window);
break;
// 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)}`);
} catch (error) {
this.addSceneEvent(`Error setting up scene content: ${error.message}`);
}
}
/**
* Note: When creating UI's with plain core, buttons will be garbage collected if not referenced.
* Particularly when opening many new scenes.
* If the button is GC'd by the system, the taps will no longer function.
* This is more related to iOS delegates getting GC'd than anything.
* Most flavors circumvent things like that because their components are retained.
* We circumvent the core demo (xml ui) issue but just retaining a map of the created UI buttons.
*/
private _closeButtons = new Map<string, Button>();
private _createPageForScene(scene: UIWindowScene, window: UIWindow): Page {
const page = new Page();
page.backgroundColor = new Color('#cdffdb');
// Create a simple layout for the new scene
const layout = new StackLayout();
layout.padding = 32;
page.content = layout;
// Add title
const title = new Label();
title.text = 'New NativeScript Scene';
title.fontSize = 35;
title.fontWeight = 'bold';
title.textAlignment = 'center';
title.marginBottom = 30;
layout.addChild(title);
// Add scene info
const sceneInfo = new Label();
sceneInfo.text = `Scene ID: ${scene.hash || 'Unknown'}\nWindow: ${window.description || 'Unknown'}`;
sceneInfo.fontSize = 22;
sceneInfo.textAlignment = 'center';
sceneInfo.marginBottom = 25;
layout.addChild(sceneInfo);
// Add close button
const closeButton = new Button();
const sceneId = this.getSceneId(scene);
closeButton.id = sceneId;
console.log('scene assigning id to button:', closeButton.id);
closeButton.text = 'Close This Scene';
closeButton.fontSize = 22;
closeButton.fontWeight = 'bold';
closeButton.backgroundColor = '#ff4444';
closeButton.color = new Color('white');
closeButton.borderRadius = 8;
closeButton.padding = 16;
closeButton.width = 300;
closeButton.horizontalAlignment = 'center';
closeButton.on('tap', this._closeScene.bind(this));
// retain the close button so we don't lose the tap (iOS delegate binding)
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;
}
private _createAltPageForScene(scene: UIWindowScene, window: UIWindow): Page {
const page = new Page();
page.backgroundColor = new Color('#65ADF1');
// Create a simple layout for the new scene
const layout = new StackLayout();
layout.padding = 32;
page.content = layout;
// Add title
const title = new Label();
title.text = 'New Alternate NativeScript Scene';
title.fontSize = 35;
title.color = new Color('blue');
title.fontWeight = 'bold';
title.textAlignment = 'center';
title.marginBottom = 35;
layout.addChild(title);
// Add scene info
const sceneInfo = new Label();
sceneInfo.text = `Scene ID: ${scene.hash || 'Unknown'}\nWindow: ${window.description || 'Unknown'}`;
sceneInfo.fontSize = 24;
sceneInfo.textAlignment = 'center';
sceneInfo.marginBottom = 32;
layout.addChild(sceneInfo);
// Add close button
const closeButton = new Button();
const sceneId = this.getSceneId(scene);
closeButton.id = sceneId;
console.log('scene assigning id to button:', closeButton.id);
closeButton.text = 'Close This Scene';
closeButton.fontSize = 25;
closeButton.fontWeight = 'bold';
closeButton.backgroundColor = '#006ead';
closeButton.color = new Color('white');
closeButton.borderRadius = 8;
closeButton.padding = 16;
closeButton.width = 350;
closeButton.horizontalAlignment = 'center';
closeButton.on('tap', this._closeScene.bind(this));
// retain the close button so we don't lose the tap (iOS delegate binding)
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;
}
private _closeScene(args: EventData) {
const btn = args.object as Button;
console.log('closing scene from button tap');
// Let core resolve the scene from the view/window context
Application.ios.closeWindow(btn);
}
private getSceneAPIInfo(): string {
if (!__APPLE__) return 'Not iOS';
try {
if (typeof UIApplication !== 'undefined') {
const app = UIApplication.sharedApplication;
if (typeof app.activateSceneSessionForRequestErrorHandler === 'function') {
return `iOS ${Utils.SDK_VERSION} - Modern API (iOS 17+) available`;
} else if (typeof app.requestSceneSessionActivationUserActivityOptionsErrorHandler === 'function') {
return `iOS ${Utils.SDK_VERSION} - Legacy API (iOS 13-16) available`;
} else {
return `iOS ${Utils.SDK_VERSION} - No scene activation API available`;
}
}
} catch (e) {
return `Error detecting API: ${e.message}`;
}
return 'Unknown API status';
}
private addSceneEvent(event: string) {
const timestamp = new Date().toLocaleTimeString();
const evt = `${timestamp}: ${event}`;
this._sceneEvents.unshift(evt);
console.log(evt);
// Keep only last 20 events
if (this._sceneEvents.length > 20) {
this._sceneEvents = this._sceneEvents.slice(0, 20);
}
this.notifyPropertyChange('sceneEvents', this._sceneEvents);
}
private updateSceneInfo() {
if (__APPLE__ && this._isMultiSceneSupported) {
try {
this._currentScenes = Application.ios.getAllScenes() || [];
this._currentWindows = Application.ios.getAllWindows() || [];
this._sceneCount = this._currentScenes.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);
}
onCreateNewScene() {
Application.ios.openWindow({ id: 'newSceneBasic' });
}
onCreateNewSceneAlt() {
Application.ios.openWindow({ id: 'newSceneAlt' });
}
onRefreshSceneInfo() {
this.updateSceneInfo();
this.addSceneEvent('Scene info refreshed');
}
onClearEvents() {
this._sceneEvents = [];
this.notifyPropertyChange('sceneEvents', this._sceneEvents);
}
onShowSceneDetails() {
if (!__APPLE__) {
Dialogs.alert({
title: 'Scene Details',
message: 'Scene functionality is only available on iOS 13+',
okButtonText: 'OK',
});
return;
}
const apiInfo = this.getSceneAPIInfo();
const sceneInfo = this.getSceneAPIInfo(); // Using getSceneAPIInfo for now
// Add SceneDelegate registration info
let delegateInfo = '\n--- SceneDelegate Status ---\n';
if (typeof global.SceneDelegate !== 'undefined') {
delegateInfo += '✅ SceneDelegate: Registered globally\n';
} else {
delegateInfo += '❌ SceneDelegate: NOT registered\n';
}
if (typeof UIWindowSceneDelegate !== 'undefined') {
delegateInfo += '✅ UIWindowSceneDelegate: Available\n';
} else {
delegateInfo += '❌ UIWindowSceneDelegate: Not available\n';
}
const fullDetails = `${sceneInfo}\n\n${apiInfo}${delegateInfo}`;
// Show in alert dialog
const alertOptions = {
title: 'Scene & API Details',
message: fullDetails,
okButtonText: 'OK',
};
Dialogs.alert(alertOptions);
}
onTestSceneAPI() {
const apiInfo = this.getSceneAPIInfo();
this.addSceneEvent(`API Test: ${apiInfo}`);
// Also log current scene/window counts
this.addSceneEvent(`Current state: ${this._currentScenes.length} scenes, ${this._currentWindows.length} windows`);
// Add device and system info
try {
const device = UIDevice.currentDevice;
this.addSceneEvent(`Device: ${device.model} (${device.systemName} ${device.systemVersion})`);
// Check if this is iPad (more likely to support multiple scenes)
if (device.userInterfaceIdiom === UIUserInterfaceIdiom.Pad) {
this.addSceneEvent('📱 iPad detected - better scene support expected');
} else {
this.addSceneEvent('📱 iPhone detected - limited scene support');
}
} catch (e) {
this.addSceneEvent('Could not get device info');
}
}
private checkSceneDelegateRegistration() {
if (!__APPLE__) {
return;
}
this.addSceneEvent('Checking SceneDelegate registration...');
// Check if SceneDelegate is available globally
if (typeof global.SceneDelegate !== 'undefined') {
this.addSceneEvent('✅ SceneDelegate is registered globally');
} else {
this.addSceneEvent('❌ SceneDelegate NOT found in global scope!');
}
// Check if UIWindowSceneDelegate is available
if (typeof UIWindowSceneDelegate !== 'undefined') {
this.addSceneEvent('✅ UIWindowSceneDelegate protocol available');
} else {
this.addSceneEvent('❌ UIWindowSceneDelegate protocol not available');
}
}
// private closeScene(scene: UIWindowScene) {
// if (!scene || !__APPLE__) return;
// try {
// this.addSceneEvent(`Attempting to close scene: ${this.getSceneDescription(scene)}`);
// // Get the scene session
// const session = scene.session;
// if (session) {
// // Check if this is the primary scene (typically can't be closed)
// const isPrimaryScene = Application.ios.getPrimaryScene() === scene;
// const sceneId = this.getSceneId(scene);
// console.log('isPrimaryScene:', isPrimaryScene, 'sceneId:', sceneId);
// if (isPrimaryScene) {
// this.addSceneEvent(`⚠️ This appears to be the primary scene`);
// this.addSceneEvent(`💡 Primary scenes typically cannot be closed programmatically`);
// return;
// } else {
// this.addSceneEvent(`✅ This appears to be a secondary scene - closure should work`);
// }
// // Try the correct iOS API for scene destruction
// const app = UIApplication.sharedApplication;
// if (app.requestSceneSessionDestructionOptionsErrorHandler) {
// this.addSceneEvent(`📞 Calling scene destruction API...`);
// app.requestSceneSessionDestructionOptionsErrorHandler(session, null, (error: NSError) => {
// if (error) {
// console.log('scene destroy error:', error);
// this.addSceneEvent(`❌ Scene destruction failed: ${error.localizedDescription}`);
// this.addSceneEvent(`📋 Error details - Domain: ${error.domain}, Code: ${error.code}`);
// // Provide specific guidance based on error
// if (error.localizedDescription.includes('primary') || error.code === 1) {
// this.addSceneEvent(`💡 Cannot close primary scene - this is iOS system behavior`);
// this.addSceneEvent(` Only secondary scenes can be closed via API`);
// } else if (error.code === 22 || error.domain.includes('FBSWorkspace')) {
// this.addSceneEvent(`💡 System declined scene destruction request`);
// this.addSceneEvent(`🔄 This may be due to system resource management`);
// } else {
// this.addSceneEvent(`🔍 Unexpected error - scene destruction may not be fully supported`);
// }
// this.addSceneEvent(`🖱️ Alternative: Use system UI to close (app switcher or split-screen controls)`);
// } else {
// this._closeButtons.delete(sceneId);
// this.addSceneEvent(`✅ Scene destruction request accepted`);
// this.addSceneEvent(`⏳ Scene should close within a few seconds...`);
// }
// });
// } else {
// this.addSceneEvent(`❌ Scene destruction API not available`);
// this.addSceneEvent(`📱 This iOS version/configuration may not support programmatic scene closure`);
// }
// } else {
// this.addSceneEvent('❌ Error: Could not find scene session to close');
// }
// } catch (error) {
// this.addSceneEvent(`❌ Error closing scene: ${error.message}`);
// }
// }
}