mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-17 21:01:34 +08:00
556 lines
15 KiB
TypeScript
556 lines
15 KiB
TypeScript
import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-helper';
|
|
import { initAccessibilityFontScale } from '../accessibility/font-scale';
|
|
import { profile } from '../profiling';
|
|
import { View } from '../ui';
|
|
import { Builder } from '../ui/builder';
|
|
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 './';
|
|
import { ApplicationCommon } from './application-common';
|
|
import {
|
|
ApplicationEventData,
|
|
LaunchEventData,
|
|
LoadAppCSSEventData,
|
|
} from './application-interfaces';
|
|
|
|
@NativeClass
|
|
class CADisplayLinkTarget extends NSObject {
|
|
private _owner: WeakRef<iOSApplication>;
|
|
static initWithOwner(owner: WeakRef<iOSApplication>): CADisplayLinkTarget {
|
|
const target = <CADisplayLinkTarget>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(<ApplicationEventData>{
|
|
eventName: owner.displayedEvent,
|
|
object: owner,
|
|
ios: owner.ios,
|
|
});
|
|
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 = <NotificationObserver>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 _backgroundColor =
|
|
Utils.ios.MajorVersion <= 12 || !UIColor.systemBackgroundColor
|
|
? UIColor.whiteColor
|
|
: UIColor.systemBackgroundColor;
|
|
private _delegate: UIApplicationDelegate;
|
|
private _window: UIWindow;
|
|
private _notificationObservers: NotificationObserver[] = [];
|
|
private _rootView: View;
|
|
|
|
displayedOnce = false;
|
|
displayedLinkTarget: CADisplayLinkTarget;
|
|
displayedLink: CADisplayLink;
|
|
|
|
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 {
|
|
console.log('run in iOSApplication', entry);
|
|
|
|
this.mainEntry = typeof entry === 'string' ? { moduleName: entry } : entry;
|
|
this.started = true;
|
|
|
|
if (this.nativeApp) {
|
|
this.runAsEmbeddedApp();
|
|
} else {
|
|
this.runAsMainApp();
|
|
}
|
|
|
|
initAccessibilityCssHelper();
|
|
initAccessibilityFontScale();
|
|
}
|
|
|
|
private runAsMainApp() {
|
|
console.log('runAsMainApp', this.delegate);
|
|
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();
|
|
if (!rootView) {
|
|
return;
|
|
}
|
|
// Attach to the existing iOS app
|
|
const window =
|
|
this.nativeApp.keyWindow ||
|
|
(this.nativeApp.windows.count > 0 && this.nativeApp.windows[0]);
|
|
|
|
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.setRootViewsSystemAppearanceCssClass(rootView);
|
|
this.notifyAppStarted();
|
|
}
|
|
|
|
private createRootView(v?: View) {
|
|
let rootView = v;
|
|
if (!rootView) {
|
|
console.log('createRootView mainEntry', this.mainEntry);
|
|
// try to navigate to the mainEntry (if specified)
|
|
if (!this.mainEntry) {
|
|
throw new Error(
|
|
'Main entry is missing. App cannot be started. Verify app bootstrap.'
|
|
);
|
|
} else {
|
|
// console.log('createRootView mainEntry:', mainEntry);
|
|
rootView = Builder.createViewFromEntry(this.mainEntry);
|
|
}
|
|
}
|
|
// console.log('createRootView rootView:', rootView);
|
|
|
|
// setRootViewsCssClasses(rootView);
|
|
|
|
return rootView;
|
|
}
|
|
|
|
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 = UIScreen.mainScreen?.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.ios.MajorVersion >= 15) {
|
|
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 {
|
|
if (NativeScriptEmbedder.sharedInstance().delegate && !this._window) {
|
|
this._window = UIApplication.sharedApplication.keyWindow;
|
|
}
|
|
|
|
return this._window;
|
|
}
|
|
|
|
get delegate(): UIApplicationDelegate {
|
|
return this._delegate;
|
|
}
|
|
|
|
set delegate(value: UIApplicationDelegate | unknown) {
|
|
if (this._delegate !== value) {
|
|
this._delegate = value as UIApplicationDelegate;
|
|
}
|
|
}
|
|
|
|
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 (Utils.ios.MajorVersion <= 11) {
|
|
return undefined;
|
|
}
|
|
|
|
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() {
|
|
const 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 args: LaunchEventData = {
|
|
eventName: this.launchEvent,
|
|
object: this,
|
|
ios:
|
|
notification?.userInfo?.objectForKey(
|
|
'UIApplicationLaunchOptionsLocalNotificationKey'
|
|
) || null,
|
|
};
|
|
|
|
this.notify(args);
|
|
this.notify(<LoadAppCSSEventData>{
|
|
eventName: 'loadAppCss',
|
|
object: this,
|
|
cssFile: this.getCssFileName(),
|
|
});
|
|
|
|
if (this._window) {
|
|
if (args.root !== null && !NativeScriptEmbedder.sharedInstance().delegate) {
|
|
this.setWindowContent(args.root);
|
|
}
|
|
} else {
|
|
this._window = UIApplication.sharedApplication.keyWindow;
|
|
}
|
|
}
|
|
|
|
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 haveController = this._window.rootViewController !== null;
|
|
this._window.rootViewController = controller;
|
|
|
|
this.setRootViewsSystemAppearanceCssClass(rootView);
|
|
|
|
if (!haveController) {
|
|
this._window.makeKeyAndVisible();
|
|
}
|
|
|
|
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();
|
|
|
|
this._window = UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds);
|
|
|
|
// TODO: Expose Window module so that it can we styled from XML & CSS
|
|
this._window.backgroundColor = this._backgroundColor;
|
|
|
|
this.notifyAppStarted(notification);
|
|
}
|
|
|
|
@profile
|
|
private didBecomeActive(notification: NSNotification) {
|
|
// const ios = UIApplication.sharedApplication;
|
|
// const object = this;
|
|
// setInBackground(false);
|
|
// setSuspended(false);
|
|
this.notify(<ApplicationEventData>{
|
|
eventName: this.resumeEvent,
|
|
object: this,
|
|
ios: this.ios,
|
|
});
|
|
this.notify(<ApplicationEventData>{
|
|
eventName: this.foregroundEvent,
|
|
object: this,
|
|
ios: this.ios,
|
|
});
|
|
// const rootView = this._rootView;
|
|
// if (rootView && !rootView.isLoaded) {
|
|
// rootView.callLoaded();
|
|
// }
|
|
}
|
|
|
|
private didEnterBackground(notification: NSNotification) {
|
|
// const ios = UIApplication.sharedApplication;
|
|
// const object = this;
|
|
// setInBackground(true);
|
|
// setSuspended(true);
|
|
|
|
this.notify(<ApplicationEventData>{
|
|
eventName: this.suspendEvent,
|
|
object: this,
|
|
ios: this.ios,
|
|
});
|
|
this.notify(<ApplicationEventData>{
|
|
eventName: this.backgroundEvent,
|
|
object: this,
|
|
ios: this.ios,
|
|
});
|
|
|
|
// const rootView = this._rootView;
|
|
// if (rootView && rootView.isLoaded) {
|
|
// rootView.callUnloaded();
|
|
// }
|
|
}
|
|
|
|
private willTerminate(notification: NSNotification) {
|
|
this.notify(<ApplicationEventData>{
|
|
eventName: this.exitEvent,
|
|
object: this,
|
|
ios: this.ios,
|
|
});
|
|
|
|
// const rootView = this._rootView;
|
|
// if (rootView && rootView.isLoaded) {
|
|
// rootView.callUnloaded();
|
|
// }
|
|
}
|
|
|
|
private didReceiveMemoryWarning(notification: NSNotification) {
|
|
this.notify(<ApplicationEventData>{
|
|
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;
|
|
}
|
|
}
|
|
export * from './application-common';
|
|
export const Application = new iOSApplication();
|
|
export const AndroidApplication = undefined;
|