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 = 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(); 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}`); // } // } }