UNPKG

@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
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;