@nativescript/core
Version:
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
1,205 lines (1,204 loc) • 61.7 kB
JavaScript
import { profile } from '../profiling';
import { isEmbedded } from '../ui/embedding';
import { IOSHelper } from '../ui/core/view/view-helper';
import { getWindow } from '../utils/native-helper';
import { SDK_VERSION } from '../utils/constants';
import { ios as iosUtils, dataSerialize } from '../utils/native-helper';
import { ApplicationCommon, initializeSdkVersionClass, SceneEvents } from './application-common';
import { Observable } from '../data/observable';
import { Trace } from '../trace';
import { AccessibilityServiceEnabledPropName, CommonA11YServiceEnabledObservable, SharedA11YObservable, a11yServiceClasses, a11yServiceDisabledClass, a11yServiceEnabledClass, fontScaleCategoryClasses, fontScaleExtraLargeCategoryClass, fontScaleExtraSmallCategoryClass, fontScaleMediumCategoryClass, getCurrentA11YServiceClass, getCurrentFontScaleCategory, getCurrentFontScaleClass, getFontScaleCssClasses, setCurrentA11YServiceClass, setCurrentFontScaleCategory, setCurrentFontScaleClass, setFontScaleCssClasses, FontScaleCategory, getClosestValidFontScale, VALID_FONT_SCALES, setFontScale, getFontScale, setInitFontScale, getFontScaleCategory, setInitAccessibilityCssHelper, notifyAccessibilityFocusState, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, isA11yEnabled, setA11yEnabled, enforceArray, } from '../accessibility/accessibility-common';
import { CoreTypes } from '../core-types';
import { getiOSWindow, setA11yUpdatePropertiesCallback, setApplicationPropertiesCallback, setAppMainEntry, setiOSWindow, setRootView, setToggleApplicationEventListenersCallback } from './helpers-common';
var NotificationObserver = (function (_super) {
__extends(NotificationObserver, _super);
function NotificationObserver() {
return _super !== null && _super.apply(this, arguments) || this;
}
NotificationObserver.initWithCallback = function (onReceiveCallback) {
var observer = _super.new.call(this);
observer._onReceiveCallback = onReceiveCallback;
return observer;
};
NotificationObserver.prototype.onReceive = function (notification) {
this._onReceiveCallback(notification);
};
NotificationObserver.ObjCExposedMethods = {
onReceive: { returns: interop.types.void, params: [NSNotification] },
};
return NotificationObserver;
}(NSObject));
var CADisplayLinkTarget = (function (_super) {
__extends(CADisplayLinkTarget, _super);
function CADisplayLinkTarget() {
return _super !== null && _super.apply(this, arguments) || this;
}
CADisplayLinkTarget.initWithOwner = function (owner) {
var target = CADisplayLinkTarget.new();
target._owner = owner;
return target;
};
CADisplayLinkTarget.prototype.onDisplayed = function (link) {
link.invalidate();
var owner = this._owner.deref();
if (!owner) {
return;
}
owner.displayedOnce = true;
owner.notify({
eventName: owner.displayedEvent,
object: owner,
ios: UIApplication.sharedApplication,
});
owner.displayedLinkTarget = null;
owner.displayedLink = null;
};
CADisplayLinkTarget.ObjCExposedMethods = {
onDisplayed: { returns: interop.types.void, params: [CADisplayLink] },
};
return CADisplayLinkTarget;
}(NSObject));
/**
* Detect if the app supports scenes.
* When an app configures UIApplicationSceneManifest in Info.plist
* it will use scene lifecycle management.
*/
let sceneManifest;
function supportsScenes() {
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() {
if (SDK_VERSION < 13) {
return false;
}
return UIApplication.sharedApplication?.supportsMultipleScenes;
}
var Responder = (function (_super) {
__extends(Responder, _super);
function Responder() {
return _super !== null && _super.apply(this, arguments) || this;
}
Object.defineProperty(Responder.prototype, "window", {
get: function () {
return Application.ios.window;
},
set: function (value) {
},
enumerable: true,
configurable: true
});
Responder.ObjCProtocols = [UIApplicationDelegate];
return Responder;
}(UIResponder));
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.applicationConfigurationForConnectingSceneSessionOptions = function (application, connectingSceneSession, options) {
const config = UISceneConfiguration.configurationWithNameSessionRole('Default Configuration', connectingSceneSession.role);
config.sceneClass = UIWindowScene;
config.delegateClass = SceneDelegate;
return config;
};
// scene session destruction handling
Responder.prototype.applicationDidDiscardSceneSessions = function (application, sceneSessions) {
// Note: we could emit an event here if needed
// console.log('Scene sessions discarded:', sceneSessions.count);
};
}
var SceneDelegate = (function (_super) {
__extends(SceneDelegate, _super);
function SceneDelegate() {
return _super !== null && _super.apply(this, arguments) || this;
}
Object.defineProperty(SceneDelegate.prototype, "window", {
get: function () {
return this._window;
},
set: function (value) {
this._window = value;
},
enumerable: true,
configurable: true
});
SceneDelegate.prototype.sceneWillConnectToSessionOptions = function (scene, session, connectionOptions) {
if (Trace.isEnabled()) {
Trace.write("SceneDelegate.sceneWillConnectToSessionOptions called with role: ".concat(session.role), Trace.categories.NativeLifecycle);
}
if (!(scene instanceof UIWindowScene)) {
return;
}
this._scene = scene;
this._window = UIWindow.alloc().initWithWindowScene(scene);
Application.ios._setWindowForScene(this._window, scene);
Application.ios._setupWindowForScene(this._window, scene);
Application.ios.notify({
eventName: SceneEvents.sceneWillConnect,
object: Application.ios,
scene: scene,
window: this._window,
connectionOptions: connectionOptions,
});
if (scene === Application.ios.getPrimaryScene()) {
this._window.makeKeyAndVisible();
}
else {
Application.ios.notify({
eventName: SceneEvents.sceneContentSetup,
object: Application.ios,
scene: scene,
window: this._window,
connectionOptions: connectionOptions,
});
}
if (!Application.ios.getPrimaryScene()) {
Application.ios._notifySceneAppStarted();
}
};
SceneDelegate.prototype.sceneDidBecomeActive = function (scene) {
};
SceneDelegate.prototype.sceneWillResignActive = function (scene) {
Application.ios.notify({
eventName: SceneEvents.sceneWillResignActive,
object: Application.ios,
scene: scene,
});
};
SceneDelegate.prototype.sceneWillEnterForeground = function (scene) {
};
SceneDelegate.prototype.sceneDidEnterBackground = function (scene) {
};
SceneDelegate.prototype.sceneDidDisconnect = function (scene) {
};
SceneDelegate.ObjCProtocols = [UIWindowSceneDelegate];
return SceneDelegate;
}(UIResponder));
// ensure available globally
global.SceneDelegate = SceneDelegate;
export class iOSApplication extends ApplicationCommon {
/**
* @internal - should not be constructed by the user.
*/
constructor() {
super();
this._delegateHandlers = new Map();
this._windowSceneMap = new Map();
this._primaryScene = null;
this._openedScenesById = new Map();
this._notificationObservers = [];
this.displayedOnce = false;
this.addNotificationObserver(UIApplicationDidFinishLaunchingNotification, this.didFinishLaunchingWithOptions.bind(this));
this.addNotificationObserver(UIApplicationDidBecomeActiveNotification, this.didBecomeActive.bind(this));
this.addNotificationObserver(UIApplicationDidEnterBackgroundNotification, this.didEnterBackground.bind(this));
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() {
return this._rootView;
}
resetRootView(view) {
super.resetRootView(view);
this.setWindowContent();
}
run(entry) {
setAppMainEntry(typeof entry === 'string' ? { moduleName: entry } : entry);
this.started = true;
if (this.nativeApp) {
this.runAsEmbeddedApp();
}
else {
this.runAsMainApp();
}
}
runAsMainApp() {
UIApplicationMain(0, null, null, this.delegate ? NSStringFromClass(this.delegate) : NSStringFromClass(Responder));
}
runAsEmbeddedApp() {
// TODO: this rootView should be held alive until rootController dismissViewController is called.
const rootView = this.createRootView(this._rootView, true);
if (!rootView) {
return;
}
this._rootView = rootView;
setRootView(rootView);
// Attach to the existing iOS app
const window = getWindow();
if (!window) {
return;
}
const rootController = window.rootViewController;
if (!rootController) {
return;
}
const controller = this.getViewController(rootView);
const embedderDelegate = NativeScriptEmbedder.sharedInstance().delegate;
rootView._setupAsRootView({});
rootView.on(IOSHelper.traitCollectionColorAppearanceChangedEvent, () => {
const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
const newSystemAppearance = this.getSystemAppearanceValue(userInterfaceStyle);
this.setSystemAppearance(newSystemAppearance);
});
rootView.on(IOSHelper.traitCollectionLayoutDirectionChangedEvent, () => {
const layoutDirection = controller.traitCollection.layoutDirection;
const newLayoutDirection = this.getLayoutDirectionValue(layoutDirection);
this.setLayoutDirection(newLayoutDirection);
});
if (embedderDelegate) {
this.setViewControllerView(rootView);
embedderDelegate.presentNativeScriptApp(controller);
}
else {
const visibleVC = iosUtils.getVisibleViewController(rootController);
visibleVC.presentViewControllerAnimatedCompletion(controller, true, null);
}
this.initRootView(rootView);
this.notifyAppStarted();
}
getViewController(rootView) {
let viewController = rootView.viewController || rootView.ios;
if (!(viewController instanceof UIViewController)) {
// We set UILayoutViewController dynamically to the root view if it doesn't have a view controller
// At the moment the root view doesn't have its native view created. We set it in the setViewControllerView func
viewController = IOSHelper.UILayoutViewController.initWithOwner(new WeakRef(rootView));
rootView.viewController = viewController;
}
return viewController;
}
setViewControllerView(view) {
const viewController = view.viewController || view.ios;
const nativeView = view.ios || view.nativeViewProtected;
if (!nativeView || !viewController) {
throw new Error('Root should be either UIViewController or UIView');
}
if (viewController instanceof IOSHelper.UILayoutViewController) {
viewController.view.addSubview(nativeView);
}
}
setMaxRefreshRate(options) {
const adjustRefreshRate = () => {
if (!this.displayedLink) {
return;
}
const minFrameRateDisabled = NSBundle.mainBundle.objectForInfoDictionaryKey('CADisableMinimumFrameDurationOnPhone');
if (minFrameRateDisabled) {
let max = 120;
const deviceMaxFrames = iosUtils.getMainScreen().maximumFramesPerSecond;
if (options?.max) {
if (deviceMaxFrames) {
// iOS 10.3
max = options.max <= deviceMaxFrames ? options.max : deviceMaxFrames;
}
else if (this.displayedLink.preferredFramesPerSecond) {
// iOS 10.0
max = options.max <= this.displayedLink.preferredFramesPerSecond ? options.max : this.displayedLink.preferredFramesPerSecond;
}
}
if (SDK_VERSION >= 15 || __VISIONOS__) {
const min = options?.min || max / 2;
const preferred = options?.preferred || max;
this.displayedLink.preferredFrameRateRange = CAFrameRateRangeMake(min, max, preferred);
}
else {
this.displayedLink.preferredFramesPerSecond = max;
}
}
};
if (this.displayedOnce) {
adjustRefreshRate();
return;
}
this.displayedLinkTarget = CADisplayLinkTarget.initWithOwner(new WeakRef(this));
this.displayedLink = CADisplayLink.displayLinkWithTargetSelector(this.displayedLinkTarget, 'onDisplayed');
adjustRefreshRate();
this.displayedLink.addToRunLoopForMode(NSRunLoop.mainRunLoop, NSDefaultRunLoopMode);
this.displayedLink.addToRunLoopForMode(NSRunLoop.mainRunLoop, UITrackingRunLoopMode);
}
get rootController() {
return this.window?.rootViewController;
}
get nativeApp() {
return UIApplication.sharedApplication;
}
get window() {
// TODO: consideration
// may not want to cache this value given the potential of multiple scenes
// particularly with SwiftUI app lifecycle based apps
if (!getiOSWindow()) {
// Note: NativeScriptViewFactory.getKeyWindow will always be used in SwiftUI app lifecycle based apps
setiOSWindow(getWindow());
}
return getiOSWindow();
}
get delegate() {
return this._delegate;
}
set delegate(value) {
if (this._delegate !== value) {
this._delegate = value;
}
}
addDelegateHandler(methodName, handler) {
// safe-guard against invalid handlers
if (typeof handler !== 'function') {
return;
}
// ensure we have a delegate
this.delegate ?? (this.delegate = Responder);
const handlers = this._delegateHandlers.get(methodName) ?? [];
if (!this._delegateHandlers.has(methodName)) {
const originalHandler = this.delegate.prototype[methodName];
if (originalHandler) {
// if there is an original handler, we add it to the handlers array to be called first.
handlers.push(originalHandler);
}
// replace the original method implementation with one that will call all handlers.
this.delegate.prototype[methodName] = function (...args) {
let res;
for (const handler of handlers) {
if (typeof handler !== 'function') {
continue;
}
res = handler.apply(this, args);
}
return res;
};
// store the handlers
this._delegateHandlers.set(methodName, handlers);
}
handlers.push(handler);
}
getNativeApplication() {
return this.nativeApp;
}
addNotificationObserver(notificationName, onReceiveCallback) {
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
this._notificationObservers.push(observer);
return observer;
}
removeNotificationObserver(observer /* NotificationObserver */, notificationName) {
const index = this._notificationObservers.indexOf(observer);
if (index >= 0) {
this._notificationObservers.splice(index, 1);
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
}
}
getSystemAppearance() {
// userInterfaceStyle is available on UITraitCollection since iOS 12.
if ((!__VISIONOS__ && SDK_VERSION <= 11) || !this.rootController) {
return null;
}
const userInterfaceStyle = this.rootController.traitCollection.userInterfaceStyle;
return this.getSystemAppearanceValue(userInterfaceStyle);
}
getSystemAppearanceValue(userInterfaceStyle) {
switch (userInterfaceStyle) {
case 2 /* UIUserInterfaceStyle.Dark */:
return 'dark';
case 1 /* UIUserInterfaceStyle.Light */:
case 0 /* UIUserInterfaceStyle.Unspecified */:
return 'light';
}
}
getLayoutDirection() {
if (!this.rootController) {
return null;
}
const layoutDirection = this.rootController.traitCollection.layoutDirection;
return this.getLayoutDirectionValue(layoutDirection);
}
getLayoutDirectionValue(layoutDirection) {
switch (layoutDirection) {
case 0 /* UITraitEnvironmentLayoutDirection.LeftToRight */:
return CoreTypes.LayoutDirection.ltr;
case 1 /* UITraitEnvironmentLayoutDirection.RightToLeft */:
return CoreTypes.LayoutDirection.rtl;
}
}
getOrientation() {
let statusBarOrientation;
if (__VISIONOS__) {
statusBarOrientation = NativeScriptEmbedder.sharedInstance().windowScene.interfaceOrientation;
}
else {
statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation;
}
return this.getOrientationValue(statusBarOrientation);
}
getOrientationValue(orientation) {
switch (orientation) {
case 3 /* UIInterfaceOrientation.LandscapeRight */:
case 4 /* UIInterfaceOrientation.LandscapeLeft */:
return 'landscape';
case 2 /* UIInterfaceOrientation.PortraitUpsideDown */:
case 1 /* UIInterfaceOrientation.Portrait */:
return 'portrait';
case 0 /* UIInterfaceOrientation.Unknown */:
return 'unknown';
}
}
notifyAppStarted(notification) {
const root = this.notifyLaunch({
ios: notification?.userInfo?.objectForKey('UIApplicationLaunchOptionsLocalNotificationKey') ?? null,
});
if (getiOSWindow()) {
if (root !== null && !isEmbedded()) {
this.setWindowContent(root);
}
}
else {
setiOSWindow(this.window);
}
}
// Public method for scene-based app startup
_notifySceneAppStarted() {
this.notifyAppStarted();
}
_onLivesync(context) {
// Handle application root module
const isAppRootModuleChanged = context && context.path && context.path.includes(this.getMainEntry().moduleName) && context.type !== 'style';
// Set window content when:
// + Application root module is changed
// + View did not handle the change
// Note:
// The case when neither app root module is changed, nor livesync is handled on View,
// then changes will not apply until navigate forward to the module.
if (isAppRootModuleChanged || (this._rootView && !this._rootView._onLivesync(context))) {
this.setWindowContent();
}
}
setWindowContent(view) {
if (this._rootView) {
// if we already have a root view, we reset it.
this._rootView._onRootViewReset();
}
const rootView = this.createRootView(view);
const controller = this.getViewController(rootView);
this._rootView = rootView;
setRootView(rootView);
// setup view as styleScopeHost
rootView._setupAsRootView({});
this.setViewControllerView(rootView);
const win = this.window;
const haveController = win.rootViewController !== null;
win.rootViewController = controller;
if (!haveController) {
win.makeKeyAndVisible();
}
this.initRootView(rootView);
rootView.on(IOSHelper.traitCollectionColorAppearanceChangedEvent, () => {
const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
const newSystemAppearance = this.getSystemAppearanceValue(userInterfaceStyle);
this.setSystemAppearance(newSystemAppearance);
});
rootView.on(IOSHelper.traitCollectionLayoutDirectionChangedEvent, () => {
const layoutDirection = controller.traitCollection.layoutDirection;
const newLayoutDirection = this.getLayoutDirectionValue(layoutDirection);
this.setLayoutDirection(newLayoutDirection);
});
}
// Observers
didFinishLaunchingWithOptions(notification) {
if (__DEV__) {
/**
* v9+ runtime crash handling
* When crash occurs during boot, we let runtime take over
*/
if (notification.userInfo) {
const isBootCrash = notification.userInfo.objectForKey('NativeScriptBootCrash');
if (isBootCrash) {
// fatal crash will show in console without app exiting
// allowing hot reload fixes to continue
return;
}
}
}
this.setMaxRefreshRate();
// 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
}
}
didBecomeActive(notification) {
const additionalData = {
ios: UIApplication.sharedApplication,
};
this.setInBackground(false, additionalData);
this.setSuspended(false, additionalData);
const rootView = this._rootView;
if (rootView && !rootView.isLoaded) {
rootView.callLoaded();
}
}
didEnterBackground(notification) {
const additionalData = {
ios: UIApplication.sharedApplication,
};
this.setInBackground(true, additionalData);
this.setSuspended(true, additionalData);
const rootView = this._rootView;
if (rootView && rootView.isLoaded) {
rootView.callUnloaded();
}
}
willTerminate(notification) {
this.notify({
eventName: this.exitEvent,
object: this,
ios: this.ios,
});
// const rootView = this._rootView;
// if (rootView && rootView.isLoaded) {
// rootView.callUnloaded();
// }
}
didReceiveMemoryWarning(notification) {
this.notify({
eventName: this.lowMemoryEvent,
object: this,
ios: this.ios,
});
}
didChangeStatusBarOrientation(notification) {
const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation;
const newOrientation = this.getOrientationValue(statusBarOrientation);
this.setOrientation(newOrientation);
}
// Scene lifecycle notification handlers
sceneWillConnect(notification) {
const scene = notification.object;
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,
});
}
sceneDidActivate(notification) {
const scene = notification.object;
this.notify({
eventName: SceneEvents.sceneDidActivate,
object: this,
scene: scene,
});
// 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();
}
}
}
sceneWillEnterForeground(notification) {
const scene = notification.object;
this.notify({
eventName: SceneEvents.sceneWillEnterForeground,
object: this,
scene: scene,
});
}
sceneDidEnterBackground(notification) {
const scene = notification.object;
this.notify({
eventName: SceneEvents.sceneDidEnterBackground,
object: this,
scene: scene,
});
// 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();
}
}
}
sceneDidDisconnect(notification) {
const scene = notification.object;
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) => {
if (err) {
console.log('Failed to activate primary scene:', err.localizedDescription);
}
});
}
else {
UIApplication.sharedApplication.requestSceneSessionActivationUserActivityOptionsErrorHandler(this._primaryScene.session, null, null, (err) => {
if (err) {
console.log('Failed to activate primary scene (legacy):', err.localizedDescription);
}
});
}
}
this.notify({
eventName: SceneEvents.sceneDidDisconnect,
object: this,
scene: scene,
});
}
// Scene management helper methods
_setWindowForScene(window, scene) {
this._windowSceneMap.set(scene, window);
}
_removeWindowForScene(scene) {
this._windowSceneMap.delete(scene);
// also untrack opened scene id
try {
const s = scene;
if (s && s.session) {
const id = this._getSceneId(s);
this._openedScenesById.delete(id);
}
}
catch { }
}
_getWindowForScene(scene) {
return this._windowSceneMap.get(scene);
}
_setupWindowForScene(window, scene) {
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() {
if (!this._sceneDelegate) {
this._sceneDelegate = SceneDelegate.new();
}
return this._sceneDelegate;
}
set sceneDelegate(value) {
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) {
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;
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')
*/
closeWindow(target) {
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) => {
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() {
return Array.from(this._windowSceneMap.values());
}
getAllScenes() {
return Array.from(this._windowSceneMap.keys());
}
getWindowScenes() {
return this.getAllScenes().filter((scene) => scene instanceof UIWindowScene);
}
getPrimaryWindow() {
if (this._primaryScene) {
return this._getWindowForScene(this._primaryScene) || getiOSWindow();
}
return getiOSWindow();
}
getPrimaryScene() {
return this._primaryScene;
}
// Scene lifecycle management
supportsScenes() {
return supportsScenes();
}
supportsMultipleScenes() {
return supportsMultipleScenes();
}
isUsingSceneLifecycle() {
return this.supportsScenes() && this._windowSceneMap.size > 0;
}
// Call this to set up scene-based configuration
configureForScenes() {
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
_getSceneId(scene) {
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
_resolveScene(target) {
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.session && target.activationState !== undefined) {
return target;
}
// UIWindow
if (target.windowScene) {
return target.windowScene;
}
// NativeScript View
if (target?.nativeViewProtected) {
const uiView = target.nativeViewProtected;
const win = uiView?.window;
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;
}
createSceneWithLegacyAPI(data) {
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) => {
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, view) {
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;
}
}
__decorate([
profile,
__metadata("design:type", Function),
__metadata("design:paramtypes", [NSNotification]),
__metadata("design:returntype", void 0)
], iOSApplication.prototype, "didFinishLaunchingWithOptions", null);
__decorate([
profile,
__metadata("design:type", Function),
__metadata("design:paramtypes", [NSNotification]),
__metadata("design:returntype", void 0)
], iOSApplication.prototype, "didBecomeActive", null);
const iosApp = new iOSApplication();
// Attach on global, so it can also be overwritten to implement different logic based on flavor
global.__onLiveSyncCore = function (context) {
iosApp._onLivesync(context);
};
export * from './application-common';
export const Application = iosApp;
export const AndroidApplication = undefined;
function fontScaleChanged(origFontScale) {
const oldValue = getFontScale();
setFontScale(getClosestValidFontScale(origFontScale));
const currentFontScale = getFontScale();
if (oldValue !== currentFontScale) {
Application.notify({
eventName: Application.fontScaleChangedEvent,
object: Application,
newValue: currentFontScale,
});
}
}
export function getCurrentFontScale() {
setupConfigListener();
return getFontScale();
}
const sizeMap = new Map([
[UIContentSizeCategoryExtraSmall, 0.5],
[UIContentSizeCategorySmall, 0.7],
[UIContentSizeCategoryMedium, 0.85],
[UIContentSizeCategoryLarge, 1],
[UIContentSizeCategoryExtraLarge, 1.15],
[UIContentSizeCategoryExtraExtraLarge, 1.3],
[UIContentSizeCategoryExtraExtraExtraLarge, 1.5],
[UIContentSizeCategoryAccessibilityMedium, 2],
[UIContentSizeCategoryAccessibilityLarge, 2.5],
[UIContentSizeCategoryAccessibilityExtraLarge, 3],
[UIContentSizeCategoryAccessibilityExtraExtraLarge, 3.5],
[UIContentSizeCategoryAccessibilityExtraExtraExtraLarge, 4],
]);
function contentSizeUpdated(fontSize) {
if (sizeMap.has(fontSize)) {
fontScaleChanged(sizeMap.get(fontSize));
return;
}
fontScaleChanged(1);
}
function useIOSFontScale() {
if (Application.ios.nativeApp) {
contentSizeUpdated(Application.ios.nativeApp.preferredContentSizeCategory);
}
else {
fontScaleChanged(1);
}
}
let fontSizeObserver;
function setupConfigListener(attempt = 0) {
if (fontSizeObserver) {
return;
}
if (!Application.ios.nativeApp) {
if (attempt > 100) {
fontScaleChanged(1);
return;
}
// Couldn't get launchEvent to trigger.
setTimeout(() => setupConfigListener(attempt + 1), 1);
return;
}
fontSizeObserver = Application.ios.addNotificationObserver(UIContentSizeCategoryDidChangeNotification, (args) => {
const fontSize = args.userInfo.valueForKey(UIContentSizeCategoryNewValueKey);
contentSizeUpdated(fontSize);
});
Application.on(Application.exitEvent, () => {
if (fontSizeObserver) {
Application.ios.removeNotificationObserver(fontSizeObserver, UIContentSizeCategoryDidChangeNotification);
fontSizeObserver = null;
}
Application.off(Application.resumeEvent, useIOSFontScale);
});
Application.on(Application.resumeEvent, useIOSFontScale);
useIOSFontScale();
}
setInitFontScale(setupConfigListener);
/**
* Convert array of values into a bitmask.
*
* @param values string values
* @param map map lower-case name to integer value.
*/
function inputArrayToBitMask(values, map) {
return (enforceArray(values)
.filter((value) => !!value)
.map((value) => `${value}`.toLocaleLowerCase())
.filter((value) => map.has(value))
.reduce((res, value) => res | map.get(value), 0) || 0);
}
let AccessibilityTraitsMap;
let RoleTypeMap;
let nativeFocusedNotificationObserver;
let lastFocusedView;
function ensureNativeClasses() {
if (AccessibilityTraitsMap && nativeFocusedNotificationObserver) {
return;
}
AccessibilityTraitsMap = new Map([
[AccessibilityTrait.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction],
[AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn],
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
]);
RoleTypeMap = new Map([
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
[AccessibilityRole.Button, UIAccessibilityTraitButton],
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
[AccessibilityRole.Header, UIAccessibilityTraitHeader],
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
[AccessibilityRole.Image, UIAccessibilityTraitImage],
[AccessibilityRole.ImageButton, UIAccessibilityTraitImage | UIAccessibilityTraitButton],
[AccessibilityRole.Link, UIAccessibilityTraitLink],
[AccessibilityRole.None, UIAccessibilityTraitNone],
[AccessibilityRole.PlaysSound, UIAccessibilityTraitPlaysSound],
[AccessibilityRole.RadioButton, UIAccessibilityTraitButton],
[AccessibilityRole.Search, UIAccessibilityTraitSearchField],
[AccessibilityRole.StaticText, UIAccessibilityTraitStaticText],
[AccessibilityRole.StartsMediaSession, UIAccessibilityTraitStartsMediaSession],
[AccessibilityRole.Summary, UIAccessibilityTraitSummaryElement],
[AccessibilityRole.Switch, UIAccessibilityTraitButton],
]);
nativeFocusedNotificationObserver = Application.ios.addNotificationObserver(UIAccessibilityElementFocusedNotification, (args) => {
const uiView = args.userInfo?.objectForKey(UIAccessibilityFocusedElementKey);
if (!uiView?.tag) {
return;
}
const rootView = Application.getRootView();
// We use the UIView's tag to find the NativeScript View by its domId.
let view = rootView.getViewByDomId(uiView?.tag);
if (!view) {
for (const modalView of rootView._getRootModalViews()) {
view = modalView.getViewByDomId(uiView?.tag);
if (view) {
break;
}
}
}
if (!view) {
return;
}
const lastView = lastFocusedView?.deref();
if (lastView && view !== lastView) {
const lastFocusedUIView = lastView.nativeViewProtected;
if (lastFocusedUIView) {
lastFocusedView = null;
notifyAccessibilityFocusState(lastView, false, true);
}
}
lastFocusedView = new WeakRef(view);
notifyAccessibilityFocusState(view, true, false);
});
Application.on(Application.exitEvent, () => {
if (nativeFocusedNotificationObserver) {
Application.ios.removeNotificationObserver(nativeFocusedNotificationObserver, UIAccessibilityElementFocusedNotification);
}
nativeFocusedNotificationObserver = null;