mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Merge branch 'main' into feat/list-view-sticky-headers
This commit is contained in:
@@ -266,4 +266,76 @@ Button {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Multiple Scenes Demo Styles */
|
||||
|
||||
.card {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 12;
|
||||
border-width: 1;
|
||||
border-color: #e9ecef;
|
||||
margin-bottom: 15;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background-color: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15;
|
||||
border-radius: 8;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
background-color: #f1f3f4;
|
||||
border-radius: 6;
|
||||
margin-bottom: 5;
|
||||
}
|
||||
|
||||
.event-item:nth-child(even) {
|
||||
background-color: #e8eaed;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1;
|
||||
border-color: #dee2e6;
|
||||
border-radius: 8;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
color: #007bff;
|
||||
border-width: 2;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
font-size: 18;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.body {
|
||||
font-size: 14;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 12;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.font-weight-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,71 @@
|
||||
import { EventData, Page, Utils } from '@nativescript/core';
|
||||
import { Application, EventData, Page, SceneEventData, SceneEvents, Utils } from '@nativescript/core';
|
||||
import { HelloWorldModel } from './main-view-model';
|
||||
|
||||
let initSceneEvents = false;
|
||||
export function navigatingTo(args: EventData) {
|
||||
const page = <Page>args.object;
|
||||
page.bindingContext = new HelloWorldModel();
|
||||
|
||||
// Testing setting window background color
|
||||
// if (global.isIOS) {
|
||||
// if (__APPLE__) {
|
||||
// Utils.ios.setWindowBackgroundColor('blue');
|
||||
// }
|
||||
|
||||
// Note: can test global scene handling by uncommenting following
|
||||
// Can also view the 'multiple-scenes' demo page in isolation
|
||||
// setupSceneEvents();
|
||||
}
|
||||
|
||||
function setupSceneEvents() {
|
||||
if (initSceneEvents) {
|
||||
return;
|
||||
}
|
||||
initSceneEvents = true;
|
||||
if (__APPLE__) {
|
||||
if (Application.ios.supportsScenes()) {
|
||||
console.log('Supports multiple scenes:', Application.ios.supportsMultipleScenes());
|
||||
// Get all windows and scenes
|
||||
const windows = Application.ios.getAllWindows();
|
||||
const scenes = Application.ios.getWindowScenes();
|
||||
const primaryWindow = Application.ios.getPrimaryWindow();
|
||||
|
||||
console.log(`App has ${windows.length} windows`);
|
||||
console.log(`App has ${scenes.length} scenes`);
|
||||
console.log('Primary window:', primaryWindow);
|
||||
|
||||
// Check if using scene lifecycle
|
||||
if (Application.ios.isUsingSceneLifecycle()) {
|
||||
console.log('App is using scene-based lifecycle');
|
||||
}
|
||||
|
||||
// Listen to scene events
|
||||
Application.on(SceneEvents.sceneWillConnect, (args: SceneEventData) => {
|
||||
console.log('New scene connecting:', args.scene);
|
||||
console.log('Window:', args.window);
|
||||
console.log('Connection options:', args.connectionOptions);
|
||||
});
|
||||
|
||||
Application.on(SceneEvents.sceneDidActivate, (args: SceneEventData) => {
|
||||
console.log('Scene became active:', args.scene);
|
||||
});
|
||||
|
||||
Application.on(SceneEvents.sceneWillResignActive, (args: SceneEventData) => {
|
||||
console.log('Scene will resign active:', args.scene);
|
||||
});
|
||||
|
||||
Application.on(SceneEvents.sceneDidEnterBackground, (args: SceneEventData) => {
|
||||
console.log('Scene entered background:', args.scene);
|
||||
});
|
||||
|
||||
Application.on(SceneEvents.sceneWillEnterForeground, (args: SceneEventData) => {
|
||||
console.log('Scene will enter foreground:', args.scene);
|
||||
});
|
||||
|
||||
Application.on(SceneEvents.sceneDidDisconnect, (args: SceneEventData) => {
|
||||
console.log('Scene disconnected:', args.scene);
|
||||
});
|
||||
} else {
|
||||
console.log('Traditional single-window app');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page" statusBarStyle="dark">
|
||||
<Page.actionBar>
|
||||
<ActionBar title="Dev Toolbox" icon="" class="action-bar">
|
||||
</ActionBar>
|
||||
@@ -19,8 +19,10 @@
|
||||
<Button text="labels" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="list-page" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="list-page-sticky" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="multiple-scenes" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="root-layout" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="scroll-view" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="status-bar" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="switch" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="touch-animations" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
<Button text="transitions" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||
|
||||
523
apps/toolbox/src/pages/multiple-scenes.ts
Normal file
523
apps/toolbox/src/pages/multiple-scenes.ts
Normal file
@@ -0,0 +1,523 @@
|
||||
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}`);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
118
apps/toolbox/src/pages/multiple-scenes.xml
Normal file
118
apps/toolbox/src/pages/multiple-scenes.xml
Normal file
@@ -0,0 +1,118 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
|
||||
<Page.actionBar>
|
||||
<ActionBar title="Multiple Scenes Demo" class="action-bar">
|
||||
</ActionBar>
|
||||
</Page.actionBar>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout class="p-20">
|
||||
|
||||
<!-- Scene Support Status -->
|
||||
<StackLayout class="card p-20 m-b-20">
|
||||
<Label text="Scene Support Status" class="h3 text-center m-b-15"/>
|
||||
<Label text="{{ statusText }}" class="body text-center m-b-15" textWrap="true"/>
|
||||
|
||||
<GridLayout columns="*, *" class="m-t-10">
|
||||
<Label col="0" text="Scene Count:" class="body"/>
|
||||
<Label col="1" text="{{ sceneCount }}" class="body font-weight-bold"/>
|
||||
</GridLayout>
|
||||
|
||||
<Button text="Refresh Info" tap="{{ onRefreshSceneInfo }}" class="btn btn-outline m-t-15"/>
|
||||
</StackLayout>
|
||||
|
||||
<!-- Scene Controls -->
|
||||
<StackLayout class="card p-20 m-b-20" visibility="{{ canCreateNewScene ? 'visible' : 'collapsed' }}">
|
||||
<Label text="Scene Controls (Test on Physical iPad Only)" class="h3 text-center m-b-15"/>
|
||||
<Label text="Create additional windows on iPad (requires scene support enabled in Info.plist)"
|
||||
class="caption text-center m-b-15" textWrap="true"/>
|
||||
|
||||
<GridLayout columns="*,*,*" class="m-t-10">
|
||||
<Button col="0" text="New Scene" tap="{{ onCreateNewScene }}" class="btn btn-primary m-r-10"/>
|
||||
<Button col="1" text="New Alternate Scene" tap="{{ onCreateNewSceneAlt }}" class="btn btn-primary m-r-10"/>
|
||||
<Button col="2" text="Scene Details" tap="{{ onShowSceneDetails }}" class="btn btn-outline m-l-10"/>
|
||||
</GridLayout>
|
||||
|
||||
<Button text="Test API Info" tap="{{ onTestSceneAPI }}" class="btn btn-outline m-t-10"/>
|
||||
</StackLayout>
|
||||
|
||||
<!-- Configuration Help -->
|
||||
<StackLayout class="card p-20 m-b-20" visibility="{{ isMultiSceneSupported ? 'collapsed' : 'visible' }}">
|
||||
<Label text="Setup Instructions" class="h3 text-center m-b-15"/>
|
||||
<Label text="To enable multi-scene support, add this to your Info.plist:"
|
||||
class="body m-b-10" textWrap="true"/>
|
||||
|
||||
<Label class="code-block m-b-15" textWrap="true">
|
||||
<Span text="<key>UIApplicationSceneManifest</key> "/>
|
||||
<Span text="<dict> "/>
|
||||
<Span text=" <key>UIApplicationSupportsMultipleScenes</key> "/>
|
||||
<Span text=" <true/> "/>
|
||||
<Span text=" <key>UISceneConfigurations</key> "/>
|
||||
<Span text=" <dict> "/>
|
||||
<Span text=" <key>UIWindowSceneSessionRoleApplication</key> "/>
|
||||
<Span text=" <array> "/>
|
||||
<Span text=" <dict> "/>
|
||||
<Span text=" <key>UISceneDelegateClassName</key> "/>
|
||||
<Span text=" <string>SceneDelegate</string> "/>
|
||||
<Span text=" </dict> "/>
|
||||
<Span text=" </array> "/>
|
||||
<Span text=" </dict> "/>
|
||||
<Span text="</dict>"/>
|
||||
</Label>
|
||||
|
||||
<Label text="After adding this configuration, rebuild and test on iPad for best multi-window experience."
|
||||
class="caption" textWrap="true"/>
|
||||
</StackLayout>
|
||||
|
||||
<!-- Scene Events Log -->
|
||||
<StackLayout class="card p-20 m-b-20">
|
||||
<GridLayout columns="*, auto" class="m-b-15">
|
||||
<Label col="0" text="Scene Events Log" class="h3"/>
|
||||
<Button col="1" text="Clear" tap="{{ onClearEvents }}" class="btn btn-outline"/>
|
||||
</GridLayout>
|
||||
|
||||
<ScrollView height="250" class="border">
|
||||
<StackLayout class="p-10">
|
||||
<Label visibility="{{ sceneEvents.length === 0 ? 'visible' : 'collapsed' }}"
|
||||
text="No scene events yet. Scene events will appear here when scenes are created, activated, or destroyed."
|
||||
class="caption text-center" textWrap="true"/>
|
||||
|
||||
<Repeater items="{{ sceneEvents }}">
|
||||
<Repeater.itemTemplate>
|
||||
<StackLayout class="event-item p-5">
|
||||
<Label text="{{ $value }}" class="caption" textWrap="true"/>
|
||||
</StackLayout>
|
||||
</Repeater.itemTemplate>
|
||||
</Repeater>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</StackLayout>
|
||||
|
||||
<!-- Usage Instructions -->
|
||||
<StackLayout class="card p-20 m-b-20">
|
||||
<Label text="How to Test" class="h3 text-center m-b-15"/>
|
||||
|
||||
<StackLayout class="m-b-10">
|
||||
<Label text="1. iPad Testing:" class="body font-weight-bold"/>
|
||||
<Label text="• Use iPad with iOS 13+ for multi-window experience" class="caption m-l-15" textWrap="true"/>
|
||||
<Label text="• iOS 17+: Uses modern activateSceneSessionForRequestErrorHandler API" class="caption m-l-15" textWrap="true"/>
|
||||
<Label text="• iOS 13-16: Uses legacy requestSceneSessionActivation API" class="caption m-l-15" textWrap="true"/>
|
||||
<Label text="• Tap 'New Scene' to request additional windows" class="caption m-l-15" textWrap="true"/>
|
||||
<Label text="• Use split view or slide over gestures" class="caption m-l-15" textWrap="true"/>
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout class="m-b-10">
|
||||
<Label text="2. iPhone Testing:" class="body font-weight-bold"/>
|
||||
<Label text="• Scene events still work but limited to single window" class="caption m-l-15" textWrap="true"/>
|
||||
<Label text="• Watch background/foreground transitions" class="caption m-l-15" textWrap="true"/>
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout>
|
||||
<Label text="3. Scene Events:" class="body font-weight-bold"/>
|
||||
<Label text="• Monitor the events log for scene lifecycle changes" class="caption m-l-15" textWrap="true"/>
|
||||
<Label text="• Each scene activation/deactivation is logged" class="caption m-l-15" textWrap="true"/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</Page>
|
||||
22
apps/toolbox/src/pages/status-bar.ts
Normal file
22
apps/toolbox/src/pages/status-bar.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Page, Observable, EventData, Dialogs, ShowModalOptions } from '@nativescript/core';
|
||||
|
||||
let page: Page;
|
||||
|
||||
export function navigatingTo(args: EventData) {
|
||||
page = <Page>args.object;
|
||||
page.bindingContext = new StatusBarModel();
|
||||
}
|
||||
|
||||
export class StatusBarModel extends Observable {
|
||||
onOpenModal() {
|
||||
page.showModal('pages/status-bar/status-bar-modal', {
|
||||
fullscreen: true,
|
||||
ios: {
|
||||
statusBarStyle: 'dark',
|
||||
},
|
||||
closeCallback(args) {
|
||||
// console.log('close modal callback', args);
|
||||
},
|
||||
} as ShowModalOptions);
|
||||
}
|
||||
}
|
||||
11
apps/toolbox/src/pages/status-bar.xml
Normal file
11
apps/toolbox/src/pages/status-bar.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page" backgroundColor="black" statusBarStyle="light">
|
||||
<!-- Note we set statusBarStyle="light" to explicitly contrast it against our black background -->
|
||||
<Page.actionBar>
|
||||
<ActionBar title="Status Bar" color="white">
|
||||
</ActionBar>
|
||||
</Page.actionBar>
|
||||
<GridLayout rows="*,auto,auto,*">
|
||||
<Label row="1" text="Note status bar color, then Open Modal" fontSize="12" class="text-center c-white" />
|
||||
<Button row="2" text="Open Modal" tap="{{ onOpenModal }}" class="btn btn-primary btn-view-demo" marginTop="20" />
|
||||
</GridLayout>
|
||||
</Page>
|
||||
22
apps/toolbox/src/pages/status-bar/status-bar-modal.ts
Normal file
22
apps/toolbox/src/pages/status-bar/status-bar-modal.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Observable, ShownModallyData, LoadEventData, Page, ShowModalOptions } from '@nativescript/core';
|
||||
|
||||
let page: Page;
|
||||
let closeCallback: Function;
|
||||
export function onShownModally(args: ShownModallyData) {
|
||||
closeCallback = args.closeCallback;
|
||||
|
||||
if (args.context) {
|
||||
args.context.shownModally = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function onLoaded(args: LoadEventData) {
|
||||
page = args.object as Page;
|
||||
page.bindingContext = new StatusBarModalPage();
|
||||
}
|
||||
|
||||
export class StatusBarModalPage extends Observable {
|
||||
close() {
|
||||
closeCallback();
|
||||
}
|
||||
}
|
||||
9
apps/toolbox/src/pages/status-bar/status-bar-modal.xml
Normal file
9
apps/toolbox/src/pages/status-bar/status-bar-modal.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="onLoaded" shownModally="onShownModally" statusBarStyle="dark">
|
||||
|
||||
<GridLayout rows="auto,auto,auto,*" verticalAlignment="top" padding="20">
|
||||
<Button text="Close" tap="{{close}}" horizontalAlignment="right" marginRight="20" />
|
||||
|
||||
<Label row="1" text="Status bar color changed to 'dark'." verticalAlignment="top" marginTop="20" class="text-center" fontSize="12" color="black" />
|
||||
<Label row="2" text="Close and watch it change back." verticalAlignment="top" marginTop="20" class="text-center" fontSize="12" color="black" marginTop="10" />
|
||||
</GridLayout>
|
||||
</Page>
|
||||
Reference in New Issue
Block a user