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; 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(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(_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 property; } } namespace collections { export function jsArrayToNSArray(str: T[]): NSArray { return NSArray.arrayWithArray(str); } export function nsArrayToJSArray(a: NSArray): Array { 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; layer?: Array }) { const viewPropertiesToMatch: Array = customProperties?.view || ['backgroundColor']; const layerPropertiesToMatch: Array = 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;