import { profile } from '../profiling'; import { View } from '../ui'; import { IOSHelper } from '../ui/core/view/view-helper'; import { NavigationEntry } from '../ui/frame/frame-interfaces'; import * as Utils from '../utils'; import type { iOSApplication as IiOSApplication } from './application'; import { ApplicationCommon } from './application-common'; import { ApplicationEventData } from './application-interfaces'; @NativeClass class CADisplayLinkTarget extends NSObject { private _owner: WeakRef; static initWithOwner(owner: WeakRef): CADisplayLinkTarget { const target = CADisplayLinkTarget.new(); target._owner = owner; return target; } onDisplayed(link: CADisplayLink) { link.invalidate(); const 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; } public static ObjCExposedMethods = { onDisplayed: { returns: interop.types.void, params: [CADisplayLink] }, }; } @NativeClass class NotificationObserver extends NSObject { private _onReceiveCallback: (notification: NSNotification) => void; public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver { const observer = super.new(); observer._onReceiveCallback = onReceiveCallback; return observer; } public onReceive(notification: NSNotification): void { this._onReceiveCallback(notification); } public static ObjCExposedMethods = { onReceive: { returns: interop.types.void, params: [NSNotification] }, }; } @NativeClass class Responder extends UIResponder implements UIApplicationDelegate { get window(): UIWindow { return Application.ios.window; } set window(value: UIWindow) { // NOOP } static ObjCProtocols = [UIApplicationDelegate]; } export class iOSApplication extends ApplicationCommon implements IiOSApplication { private _delegate: UIApplicationDelegate; private _delegateHandlers = new Map>(); private _window: UIWindow; private _notificationObservers: NotificationObserver[] = []; private _rootView: View; displayedOnce = false; displayedLinkTarget: CADisplayLinkTarget; displayedLink: CADisplayLink; /** * @internal - should not be constructed by the user. */ constructor() { super(); 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)); } getRootView(): View { return this._rootView; } resetRootView(view?: View) { super.resetRootView(view); this.setWindowContent(); } run(entry?: string | NavigationEntry): void { this.mainEntry = typeof entry === 'string' ? { moduleName: entry } : entry; this.started = true; if (this.nativeApp) { this.runAsEmbeddedApp(); } else { this.runAsMainApp(); } } private runAsMainApp() { UIApplicationMain(0, null, null, this.delegate ? NSStringFromClass(this.delegate as any) : NSStringFromClass(Responder)); } private 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; // Attach to the existing iOS app const window = Utils.ios.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); }); if (embedderDelegate) { this.setViewControllerView(rootView); embedderDelegate.presentNativeScriptApp(controller); } else { const visibleVC = Utils.ios.getVisibleViewController(rootController); visibleVC.presentViewControllerAnimatedCompletion(controller, true, null); } this.initRootView(rootView); this.notifyAppStarted(); } private getViewController(rootView: View): UIViewController { let viewController: UIViewController = 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)) as UIViewController; rootView.viewController = viewController; } return viewController; } private setViewControllerView(view: View): void { const viewController: UIViewController = 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?: { min?: number; max?: number; preferred?: number }): void { const adjustRefreshRate = () => { if (!this.displayedLink) { return; } const minFrameRateDisabled = NSBundle.mainBundle.objectForInfoDictionaryKey('CADisableMinimumFrameDurationOnPhone'); if (minFrameRateDisabled) { let max = 120; const deviceMaxFrames = Utils.ios.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 (Utils.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(): UIWindow { // TODO: consideration // may not want to cache this value given the potential of multiple scenes // particularly with SwiftUI app lifecycle based apps if (!this._window) { // Note: NativeScriptViewFactory.getKeyWindow will always be used in SwiftUI app lifecycle based apps this._window = Utils.ios.getWindow(); } return this._window; } get delegate(): UIApplicationDelegate & { prototype: UIApplicationDelegate } { return this._delegate as any; } set delegate(value: UIApplicationDelegate | unknown) { if (this._delegate !== value) { this._delegate = value as UIApplicationDelegate; } } addDelegateHandler(methodName: T, handler: (typeof UIApplicationDelegate.prototype)[T]): void { // safe-guard against invalid handlers if (typeof handler !== 'function') { return; } // ensure we have a delegate this.delegate ??= Responder as any; 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 as Function); } // replace the original method implementation with one that will call all handlers. this.delegate.prototype[methodName] = function (...args: any[]) { let res: any; for (const handler of handlers) { if (typeof handler !== 'function') { continue; } res = handler.apply(this, args); } return res; } as (typeof UIApplicationDelegate.prototype)[T]; // store the handlers this._delegateHandlers.set(methodName, handlers); } handlers.push(handler); } getNativeApplication() { return this.nativeApp; } addNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) { const observer = NotificationObserver.initWithCallback(onReceiveCallback); NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null); this._notificationObservers.push(observer); return observer; } removeNotificationObserver(observer: any, notificationName: string) { const index = this._notificationObservers.indexOf(observer); if (index >= 0) { this._notificationObservers.splice(index, 1); NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null); } } protected getSystemAppearance(): 'light' | 'dark' { // userInterfaceStyle is available on UITraitCollection since iOS 12. if ((!__VISIONOS__ && Utils.SDK_VERSION <= 11) || !this.rootController) { return null; } const userInterfaceStyle = this.rootController.traitCollection.userInterfaceStyle; return this.getSystemAppearanceValue(userInterfaceStyle); } private getSystemAppearanceValue(userInterfaceStyle: number): 'dark' | 'light' { switch (userInterfaceStyle) { case UIUserInterfaceStyle.Dark: return 'dark'; case UIUserInterfaceStyle.Light: case UIUserInterfaceStyle.Unspecified: return 'light'; } } protected getOrientation() { let statusBarOrientation: UIInterfaceOrientation; if (__VISIONOS__) { statusBarOrientation = NativeScriptEmbedder.sharedInstance().windowScene.interfaceOrientation; } else { statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; } return this.getOrientationValue(statusBarOrientation); } private getOrientationValue(orientation: number): 'portrait' | 'landscape' | 'unknown' { switch (orientation) { case UIInterfaceOrientation.LandscapeRight: case UIInterfaceOrientation.LandscapeLeft: return 'landscape'; case UIInterfaceOrientation.PortraitUpsideDown: case UIInterfaceOrientation.Portrait: return 'portrait'; case UIInterfaceOrientation.Unknown: return 'unknown'; } } private notifyAppStarted(notification?: NSNotification) { const root = this.notifyLaunch({ ios: notification?.userInfo?.objectForKey('UIApplicationLaunchOptionsLocalNotificationKey') ?? null, }); if (this._window) { if (root !== null && !NativeScriptEmbedder.sharedInstance().delegate) { this.setWindowContent(root); } } else { this._window = this.window; // UIApplication.sharedApplication.keyWindow; } } public _onLivesync(context?: ModuleContext): void { // 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(); } } private setWindowContent(view?: View): void { 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; // 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); }); } // Observers @profile private didFinishLaunchingWithOptions(notification: NSNotification) { this.setMaxRefreshRate(); // ensures window is assigned to proper window scene this._window = this.window; if (!this._window) { // if still no window, create one this._window = UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds); } if (!__VISIONOS__) { this.window.backgroundColor = Utils.SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor; } this.notifyAppStarted(notification); } @profile private didBecomeActive(notification: NSNotification) { const additionalData = { ios: UIApplication.sharedApplication, }; this.setInBackground(false, additionalData); this.setSuspended(false, additionalData); const rootView = this._rootView; if (rootView && !rootView.isLoaded) { rootView.callLoaded(); } } private didEnterBackground(notification: NSNotification) { const additionalData = { ios: UIApplication.sharedApplication, }; this.setInBackground(true, additionalData); this.setSuspended(true, additionalData); const rootView = this._rootView; if (rootView && rootView.isLoaded) { rootView.callUnloaded(); } } private willTerminate(notification: NSNotification) { this.notify({ eventName: this.exitEvent, object: this, ios: this.ios, }); // const rootView = this._rootView; // if (rootView && rootView.isLoaded) { // rootView.callUnloaded(); // } } private didReceiveMemoryWarning(notification: NSNotification) { this.notify({ eventName: this.lowMemoryEvent, object: this, ios: this.ios, }); } private didChangeStatusBarOrientation(notification: NSNotification) { const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; const newOrientation = this.getOrientationValue(statusBarOrientation); this.setOrientation(newOrientation); } get ios() { // ensures Application.ios is defined when running on iOS return this; } } const iosApp = new iOSApplication(); // Attach on global, so it can also be overwritten to implement different logic based on flavor global.__onLiveSyncCore = function (context?: ModuleContext) { iosApp._onLivesync(context); }; export * from './application-common'; export const Application = iosApp; export const AndroidApplication = undefined;