mirror of
				https://github.com/NativeScript/NativeScript.git
				synced 2025-11-04 21:06:45 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			384 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			384 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { platformCheck } from './platform-check';
 | 
						|
import { getClass, isNullOrUndefined, numberHasDecimals, numberIs64Bit } from './types';
 | 
						|
import { Color } from '../color';
 | 
						|
import { Trace } from '../trace';
 | 
						|
import { CORE_ANIMATION_DEFAULTS, getDurationWithDampingFromSpring } from './animation-helpers';
 | 
						|
import { SDK_VERSION } from './constants';
 | 
						|
 | 
						|
export function dataDeserialize(nativeData?: any) {
 | 
						|
	if (isNullOrUndefined(nativeData)) {
 | 
						|
		// some native values will already be js null values
 | 
						|
		// calling types.getClass below on null/undefined will cause crash
 | 
						|
		return null;
 | 
						|
	} else {
 | 
						|
		switch (getClass(nativeData)) {
 | 
						|
			case 'NSNull':
 | 
						|
				return null;
 | 
						|
			case 'NSMutableDictionary':
 | 
						|
			case 'NSDictionary': {
 | 
						|
				const obj = {};
 | 
						|
				const length = nativeData.count;
 | 
						|
				const keysArray = nativeData.allKeys as NSArray<any>;
 | 
						|
				for (let i = 0; i < length; i++) {
 | 
						|
					const nativeKey = keysArray.objectAtIndex(i);
 | 
						|
					obj[nativeKey] = dataDeserialize(nativeData.objectForKey(nativeKey));
 | 
						|
				}
 | 
						|
				return obj;
 | 
						|
			}
 | 
						|
			case 'NSMutableArray':
 | 
						|
			case 'NSArray': {
 | 
						|
				const array = [];
 | 
						|
				const len = nativeData.count;
 | 
						|
				for (let i = 0; i < len; i++) {
 | 
						|
					array[i] = dataDeserialize(nativeData.objectAtIndex(i));
 | 
						|
				}
 | 
						|
				return array;
 | 
						|
			}
 | 
						|
			default:
 | 
						|
				return nativeData;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export function dataSerialize(data: any, wrapPrimitives: boolean = false) {
 | 
						|
	switch (typeof data) {
 | 
						|
		case 'string':
 | 
						|
		case 'boolean': {
 | 
						|
			return data;
 | 
						|
		}
 | 
						|
		case 'number': {
 | 
						|
			const hasDecimals = numberHasDecimals(data);
 | 
						|
			if (numberIs64Bit(data)) {
 | 
						|
				if (hasDecimals) {
 | 
						|
					return NSNumber.alloc().initWithDouble(data);
 | 
						|
				} else {
 | 
						|
					return NSNumber.alloc().initWithLongLong(data);
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if (hasDecimals) {
 | 
						|
					return NSNumber.alloc().initWithFloat(data);
 | 
						|
				} else {
 | 
						|
					return data;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		case 'object': {
 | 
						|
			if (data instanceof Date) {
 | 
						|
				return NSDate.dateWithTimeIntervalSince1970(data.getTime() / 1000);
 | 
						|
			}
 | 
						|
 | 
						|
			if (!data) {
 | 
						|
				return null;
 | 
						|
			}
 | 
						|
 | 
						|
			if (Array.isArray(data)) {
 | 
						|
				return NSArray.arrayWithArray(data.map((el) => dataSerialize(el, wrapPrimitives)).filter((el) => el !== null));
 | 
						|
			}
 | 
						|
 | 
						|
			const node = Object.fromEntries(
 | 
						|
				Object.entries(data)
 | 
						|
					.map(([key, value]) => [key, dataSerialize(value, wrapPrimitives)])
 | 
						|
					.filter(([, value]) => value !== null),
 | 
						|
			);
 | 
						|
 | 
						|
			// cast to any avoids signature overload on tsc build
 | 
						|
			return NSDictionary.dictionaryWithDictionary(node as any);
 | 
						|
		}
 | 
						|
 | 
						|
		default:
 | 
						|
			return null;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function getCurrentAppPath(): string {
 | 
						|
	if (!global.__dirname) {
 | 
						|
		global.__dirname = typeof __dirname !== 'undefined' ? __dirname : import.meta.dirname;
 | 
						|
	}
 | 
						|
	const currentDir = global.__dirname;
 | 
						|
	const tnsModulesIndex = currentDir.indexOf('/tns_modules');
 | 
						|
 | 
						|
	// Module not hosted in ~/tns_modules when bundled. Use current dir.
 | 
						|
	let appPath = currentDir;
 | 
						|
	if (tnsModulesIndex !== -1) {
 | 
						|
		// Strip part after tns_modules to obtain app root
 | 
						|
		appPath = currentDir.substring(0, tnsModulesIndex);
 | 
						|
	}
 | 
						|
 | 
						|
	return appPath;
 | 
						|
}
 | 
						|
 | 
						|
function joinPaths(...paths: string[]): string {
 | 
						|
	if (!paths || paths.length === 0) {
 | 
						|
		return '';
 | 
						|
	}
 | 
						|
 | 
						|
	return NSString.stringWithString(NSString.pathWithComponents(<any>paths)).stringByStandardizingPath;
 | 
						|
}
 | 
						|
 | 
						|
declare let UIImagePickerControllerSourceType: any;
 | 
						|
 | 
						|
const radToDeg = Math.PI / 180;
 | 
						|
 | 
						|
function isOrientationLandscape(orientation: number) {
 | 
						|
	return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || orientation === UIDeviceOrientation.LandscapeRight /* 4 */;
 | 
						|
}
 | 
						|
 | 
						|
function openFileAtRootModule(filePath: string): boolean {
 | 
						|
	try {
 | 
						|
		const appPath = getCurrentAppPath();
 | 
						|
		const path = isRealDevice() ? filePath.replace('~', appPath) : filePath;
 | 
						|
 | 
						|
		const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path));
 | 
						|
		controller.delegate = createUIDocumentInteractionControllerDelegate();
 | 
						|
 | 
						|
		return controller.presentPreviewAnimated(true);
 | 
						|
	} catch (e) {
 | 
						|
		Trace.write('Error in openFile', Trace.categories.Error, Trace.messageType.error);
 | 
						|
	}
 | 
						|
 | 
						|
	return false;
 | 
						|
}
 | 
						|
// TODO: remove for NativeScript 9.0
 | 
						|
export function getter<T>(_this: any, property: T | { (): T }): T {
 | 
						|
	console.log('utils.ios.getter() is deprecated; use the respective native property instead');
 | 
						|
	if (typeof property === 'function') {
 | 
						|
		return (<{ (): T }>property).call(_this);
 | 
						|
	} else {
 | 
						|
		return <T>property;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
namespace collections {
 | 
						|
	export function jsArrayToNSArray<T>(str: T[]): NSArray<T> {
 | 
						|
		return NSArray.arrayWithArray(str);
 | 
						|
	}
 | 
						|
 | 
						|
	export function nsArrayToJSArray<T>(a: NSArray<T>): Array<T> {
 | 
						|
		const arr = [];
 | 
						|
		if (a !== undefined) {
 | 
						|
			const count = a.count;
 | 
						|
			for (let i = 0; i < count; i++) {
 | 
						|
				arr.push(a.objectAtIndex(i));
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return arr;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function getRootViewController(): UIViewController {
 | 
						|
	const win = getWindow();
 | 
						|
	let vc = win && win.rootViewController;
 | 
						|
	while (vc && vc.presentedViewController) {
 | 
						|
		vc = vc.presentedViewController;
 | 
						|
	}
 | 
						|
	return vc;
 | 
						|
}
 | 
						|
 | 
						|
export function getWindow(): UIWindow {
 | 
						|
	let window: UIWindow;
 | 
						|
	if (SDK_VERSION >= 15 && typeof NativeScriptViewFactory !== 'undefined') {
 | 
						|
		// UIWindowScene.keyWindow is only available 15+
 | 
						|
		window = NativeScriptViewFactory.getKeyWindow();
 | 
						|
	}
 | 
						|
	if (window) {
 | 
						|
		return window;
 | 
						|
	}
 | 
						|
	const app = UIApplication.sharedApplication;
 | 
						|
	if (!app) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	return app.keyWindow || (app.windows && app.windows.count > 0 && app.windows.objectAtIndex(0));
 | 
						|
}
 | 
						|
 | 
						|
function getMainScreen(): UIScreen {
 | 
						|
	const window = getWindow();
 | 
						|
	return window ? window.screen : UIScreen.mainScreen;
 | 
						|
}
 | 
						|
 | 
						|
function setWindowBackgroundColor(value: string) {
 | 
						|
	const win = getWindow();
 | 
						|
	if (win) {
 | 
						|
		const bgColor = new Color(value);
 | 
						|
		win.backgroundColor = bgColor.ios;
 | 
						|
		const rootVc = getRootViewController();
 | 
						|
		if (rootVc?.view) {
 | 
						|
			rootVc.view.backgroundColor = bgColor.ios;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function isLandscape(): boolean {
 | 
						|
	console.log('utils.ios.isLandscape() is deprecated; use application.orientation instead');
 | 
						|
 | 
						|
	const deviceOrientation = UIDevice.currentDevice.orientation;
 | 
						|
	const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation;
 | 
						|
 | 
						|
	const isDeviceOrientationLandscape = isOrientationLandscape(deviceOrientation);
 | 
						|
	const isStatusBarOrientationLandscape = isOrientationLandscape(statusBarOrientation);
 | 
						|
 | 
						|
	return isDeviceOrientationLandscape || isStatusBarOrientationLandscape;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @deprecated use Utils.SDK_VERSION instead which is a float of the {major}.{minor} verison
 | 
						|
 */
 | 
						|
const MajorVersion = NSString.stringWithString(UIDevice.currentDevice.systemVersion).intValue;
 | 
						|
 | 
						|
export function openFile(filePath: string): boolean {
 | 
						|
	console.log('utils.ios.openFile() is deprecated; use utils.openFile() instead');
 | 
						|
 | 
						|
	return openFileAtRootModule(filePath);
 | 
						|
}
 | 
						|
 | 
						|
function getVisibleViewController(rootViewController: UIViewController): UIViewController {
 | 
						|
	let viewController = rootViewController;
 | 
						|
 | 
						|
	while (viewController && viewController.presentedViewController) {
 | 
						|
		viewController = viewController.presentedViewController;
 | 
						|
	}
 | 
						|
	return viewController;
 | 
						|
}
 | 
						|
 | 
						|
function applyRotateTransform(transform: CATransform3D, x: number, y: number, z: number): CATransform3D {
 | 
						|
	if (x) {
 | 
						|
		transform = CATransform3DRotate(transform, x * radToDeg, 1, 0, 0);
 | 
						|
	}
 | 
						|
 | 
						|
	if (y) {
 | 
						|
		transform = CATransform3DRotate(transform, y * radToDeg, 0, 1, 0);
 | 
						|
	}
 | 
						|
 | 
						|
	if (z) {
 | 
						|
		transform = CATransform3DRotate(transform, z * radToDeg, 0, 0, 1);
 | 
						|
	}
 | 
						|
 | 
						|
	return transform;
 | 
						|
}
 | 
						|
 | 
						|
function createUIDocumentInteractionControllerDelegate(): NSObject {
 | 
						|
	@NativeClass
 | 
						|
	class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate {
 | 
						|
		public static ObjCProtocols = [UIDocumentInteractionControllerDelegate];
 | 
						|
 | 
						|
		public getViewController(): UIViewController {
 | 
						|
			return getWindow().rootViewController;
 | 
						|
		}
 | 
						|
 | 
						|
		public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) {
 | 
						|
			return this.getViewController();
 | 
						|
		}
 | 
						|
 | 
						|
		public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) {
 | 
						|
			return this.getViewController().view;
 | 
						|
		}
 | 
						|
 | 
						|
		public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect {
 | 
						|
			return this.getViewController().view.frame;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return new UIDocumentInteractionControllerDelegateImpl();
 | 
						|
}
 | 
						|
 | 
						|
export function isRealDevice() {
 | 
						|
	try {
 | 
						|
		if (NSProcessInfo.processInfo.environment.valueForKey('SIMULATOR_DEVICE_NAME')) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
		return true;
 | 
						|
	} catch (e) {
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function printCGRect(rect: CGRect) {
 | 
						|
	if (rect) {
 | 
						|
		return `CGRect(${rect.origin.x} ${rect.origin.y} ${rect.size.width} ${rect.size.height})`;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
function snapshotView(view: UIView, scale: number): UIImage {
 | 
						|
	if (view instanceof UIImageView) {
 | 
						|
		return view.image;
 | 
						|
	}
 | 
						|
	// console.log('snapshotView view.frame:', printRect(view.frame));
 | 
						|
	const originalOpacity = view.layer.opacity;
 | 
						|
	view.layer.opacity = originalOpacity > 0 ? originalOpacity : 1;
 | 
						|
	UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, scale);
 | 
						|
	view.layer.renderInContext(UIGraphicsGetCurrentContext());
 | 
						|
	const image = UIGraphicsGetImageFromCurrentImageContext();
 | 
						|
	UIGraphicsEndImageContext();
 | 
						|
	setTimeout(() => {
 | 
						|
		// ensure set back properly on next tick
 | 
						|
		view.layer.opacity = originalOpacity;
 | 
						|
	});
 | 
						|
	return image;
 | 
						|
}
 | 
						|
 | 
						|
function copyLayerProperties(view: UIView, toView: UIView, customProperties?: { view?: Array<keyof UIView>; layer?: Array<keyof CALayer> }) {
 | 
						|
	const viewPropertiesToMatch: Array<keyof UIView> = customProperties?.view || ['backgroundColor'];
 | 
						|
	const layerPropertiesToMatch: Array<keyof CALayer> = customProperties?.layer || ['cornerRadius', 'borderWidth', 'borderColor'];
 | 
						|
 | 
						|
	viewPropertiesToMatch.forEach((property) => {
 | 
						|
		if (view[property] !== toView[property]) {
 | 
						|
			// console.log('|    -- matching view property:', property);
 | 
						|
			view[property as any] = toView[property];
 | 
						|
		}
 | 
						|
	});
 | 
						|
 | 
						|
	layerPropertiesToMatch.forEach((property) => {
 | 
						|
		if (view.layer[property] !== toView.layer[property]) {
 | 
						|
			// console.log('|    -- matching layer property:', property);
 | 
						|
			view.layer[property as any] = toView.layer[property];
 | 
						|
		}
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
function animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void }) {
 | 
						|
	// for convenience, default spring settings are provided
 | 
						|
	const opt = {
 | 
						|
		...CORE_ANIMATION_DEFAULTS.spring,
 | 
						|
		delay: 0,
 | 
						|
		animateOptions: null,
 | 
						|
		animations: null,
 | 
						|
		completion: null,
 | 
						|
		...(options || {}),
 | 
						|
	};
 | 
						|
	const { duration, damping } = getDurationWithDampingFromSpring(opt);
 | 
						|
 | 
						|
	if (duration === 0) {
 | 
						|
		UIView.animateWithDurationAnimationsCompletion(0, opt.animations, opt.completion);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(duration, opt.delay, damping, opt.velocity, opt.animateOptions, opt.animations, opt.completion);
 | 
						|
}
 | 
						|
 | 
						|
// these don't exist on iOS. Stub them to empty functions.
 | 
						|
export const ad = platformCheck('Utils.ad');
 | 
						|
export const android = platformCheck('Utils.android');
 | 
						|
 | 
						|
export const ios = {
 | 
						|
	collections,
 | 
						|
	createUIDocumentInteractionControllerDelegate,
 | 
						|
	getCurrentAppPath,
 | 
						|
	getRootViewController,
 | 
						|
	getVisibleViewController,
 | 
						|
	getWindow,
 | 
						|
	getMainScreen,
 | 
						|
	setWindowBackgroundColor,
 | 
						|
	isLandscape,
 | 
						|
	applyRotateTransform,
 | 
						|
	snapshotView,
 | 
						|
	joinPaths,
 | 
						|
	printCGRect,
 | 
						|
	copyLayerProperties,
 | 
						|
	animateWithSpring,
 | 
						|
	MajorVersion,
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * @deprecated Use `Utils.ios` instead.
 | 
						|
 */
 | 
						|
export const iOSNativeHelper = ios;
 |